Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176
This commit is contained in:
commit
9921556621
13277 changed files with 1459781 additions and 0 deletions
40
core/modules/editor/config/schema/editor.schema.yml
Normal file
40
core/modules/editor/config/schema/editor.schema.yml
Normal file
|
@ -0,0 +1,40 @@
|
|||
# Schema for the configuration files of the Editor module.
|
||||
|
||||
editor.editor.*:
|
||||
type: config_entity
|
||||
label: 'Text editor'
|
||||
mapping:
|
||||
format:
|
||||
type: string
|
||||
label: 'Name'
|
||||
editor:
|
||||
type: string
|
||||
label: 'Text editor'
|
||||
settings:
|
||||
type: editor.settings.[%parent.editor]
|
||||
image_upload:
|
||||
type: mapping
|
||||
label: 'Image upload settings'
|
||||
mapping:
|
||||
status:
|
||||
type: boolean
|
||||
label: 'Status'
|
||||
scheme:
|
||||
type: string
|
||||
label: 'File storage'
|
||||
directory:
|
||||
type: string
|
||||
label: 'Upload directory'
|
||||
max_size:
|
||||
type: string
|
||||
label: 'Maximum file size'
|
||||
max_dimensions:
|
||||
type: mapping
|
||||
label: 'Maximum dimensions'
|
||||
mapping:
|
||||
width:
|
||||
type: integer
|
||||
label: 'Maximum width'
|
||||
height:
|
||||
type: integer
|
||||
label: 'Maximum height'
|
16
core/modules/editor/css/editor.css
Normal file
16
core/modules/editor/css/editor.css
Normal file
|
@ -0,0 +1,16 @@
|
|||
/**
|
||||
* @file
|
||||
* Styles for text editors.
|
||||
*/
|
||||
|
||||
.editor-dialog {
|
||||
/* This !important is required to override inline CSS of jQuery UI. */
|
||||
width: 80% !important;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
.editor-dialog {
|
||||
width: 95% !important;
|
||||
}
|
||||
}
|
128
core/modules/editor/editor.admin.inc
Normal file
128
core/modules/editor/editor.admin.inc
Normal file
|
@ -0,0 +1,128 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Administration functions for editor.module.
|
||||
*/
|
||||
|
||||
use Drupal\Core\StreamWrapper\StreamWrapperInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Subform constructor to configure the text editor's image upload settings.
|
||||
*
|
||||
* Each text editor plugin that is configured to offer the ability to insert
|
||||
* images and uses EditorImageDialog for that, should use this form to update
|
||||
* the text editor's configuration so that EditorImageDialog knows whether it
|
||||
* should allow the user to upload images.
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* The text editor entity that is being edited.
|
||||
*
|
||||
* @return array
|
||||
* The image upload settings form.
|
||||
*
|
||||
* @see \Drupal\editor\Form\EditorImageDialog
|
||||
*/
|
||||
function editor_image_upload_settings_form(Editor $editor) {
|
||||
// Defaults.
|
||||
$image_upload = $editor->getImageUploadSettings();
|
||||
$image_upload += array(
|
||||
'status' => FALSE,
|
||||
'scheme' => file_default_scheme(),
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => '',
|
||||
'max_dimensions' => array('width' => '', 'height' => ''),
|
||||
);
|
||||
|
||||
$form['status'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Enable image uploads'),
|
||||
'#default_value' => $image_upload['status'],
|
||||
'#attributes' => array(
|
||||
'data-editor-image-upload' => 'status',
|
||||
),
|
||||
);
|
||||
$show_if_image_uploads_enabled = array(
|
||||
'visible' => array(
|
||||
':input[data-editor-image-upload="status"]' => array('checked' => TRUE),
|
||||
),
|
||||
);
|
||||
|
||||
// Any visible, writable wrapper can potentially be used for uploads,
|
||||
// including a remote file system that integrates with a CDN.
|
||||
$options = \Drupal::service('stream_wrapper_manager')->getDescriptions(StreamWrapperInterface::WRITE_VISIBLE);
|
||||
if (!empty($options)) {
|
||||
$form['scheme'] = array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('File storage'),
|
||||
'#default_value' => $image_upload['scheme'],
|
||||
'#options' => $options,
|
||||
'#states' => $show_if_image_uploads_enabled,
|
||||
'#access' => count($options) > 1,
|
||||
);
|
||||
}
|
||||
// Set data- attributes with human-readable names for all possible stream
|
||||
// wrappers, so that drupal.ckeditor.drupalimage.admin's summary rendering
|
||||
// can use that.
|
||||
foreach (\Drupal::service('stream_wrapper_manager')->getNames(StreamWrapperInterface::WRITE_VISIBLE) as $scheme => $name) {
|
||||
$form['scheme'][$scheme]['#attributes']['data-label'] = t('Storage: @name', array('@name' => $name));
|
||||
}
|
||||
|
||||
$form['directory'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#default_value' => $image_upload['directory'],
|
||||
'#title' => t('Upload directory'),
|
||||
'#description' => t("A directory relative to Drupal's files directory where uploaded images will be stored."),
|
||||
'#states' => $show_if_image_uploads_enabled,
|
||||
);
|
||||
|
||||
$default_max_size = format_size(file_upload_max_size());
|
||||
$form['max_size'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#default_value' => $image_upload['max_size'],
|
||||
'#title' => t('Maximum file size'),
|
||||
'#description' => t('If this is left empty, then the file size will be limited by the PHP maximum upload size of @size.', array('@size' => $default_max_size)),
|
||||
'#maxlength' => 20,
|
||||
'#size' => 10,
|
||||
'#placeholder' => $default_max_size,
|
||||
'#states' => $show_if_image_uploads_enabled,
|
||||
);
|
||||
|
||||
$form['max_dimensions'] = array(
|
||||
'#type' => 'item',
|
||||
'#title' => t('Maximum dimensions'),
|
||||
'#field_prefix' => '<div class="container-inline clearfix">',
|
||||
'#field_suffix' => '</div>',
|
||||
'#description' => t('Images larger than these dimensions will be scaled down.'),
|
||||
'#states' => $show_if_image_uploads_enabled,
|
||||
);
|
||||
$form['max_dimensions']['width'] = array(
|
||||
'#title' => t('Width'),
|
||||
'#title_display' => 'invisible',
|
||||
'#type' => 'number',
|
||||
'#default_value' => (empty($image_upload['max_dimensions']['width'])) ? '' : $image_upload['max_dimensions']['width'],
|
||||
'#size' => 8,
|
||||
'#maxlength' => 8,
|
||||
'#min' => 1,
|
||||
'#max' => 99999,
|
||||
'#placeholder' => t('width'),
|
||||
'#field_suffix' => ' x ',
|
||||
'#states' => $show_if_image_uploads_enabled,
|
||||
);
|
||||
$form['max_dimensions']['height'] = array(
|
||||
'#title' => t('Height'),
|
||||
'#title_display' => 'invisible',
|
||||
'#type' => 'number',
|
||||
'#default_value' => (empty($image_upload['max_dimensions']['height'])) ? '' : $image_upload['max_dimensions']['height'],
|
||||
'#size' => 8,
|
||||
'#maxlength' => 8,
|
||||
'#min' => 1,
|
||||
'#max' => 99999,
|
||||
'#placeholder' => t('height'),
|
||||
'#field_suffix' => t('pixels'),
|
||||
'#states' => $show_if_image_uploads_enabled,
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
70
core/modules/editor/editor.api.php
Normal file
70
core/modules/editor/editor.api.php
Normal file
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Documentation for Text Editor API.
|
||||
*/
|
||||
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Performs alterations on text editor definitions.
|
||||
*
|
||||
* @param array $editors
|
||||
* An array of metadata of text editors, as collected by the plugin annotation
|
||||
* discovery mechanism.
|
||||
*
|
||||
* @see \Drupal\editor\Plugin\EditorBase
|
||||
*/
|
||||
function hook_editor_info_alter(array &$editors) {
|
||||
$editors['some_other_editor']['label'] = t('A different name');
|
||||
$editors['some_other_editor']['library']['module'] = 'myeditoroverride';
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies JavaScript settings that are added for text editors.
|
||||
*
|
||||
* @param array $settings
|
||||
* All the settings that will be added to the page for the text formats to
|
||||
* which a user has access.
|
||||
*/
|
||||
function hook_editor_js_settings_alter(array &$settings) {
|
||||
if (isset($settings['editor']['formats']['basic_html'])) {
|
||||
$settings['editor']['formats']['basic_html']['editor'] = 'MyDifferentEditor';
|
||||
$settings['editor']['formats']['basic_html']['editorSettings']['buttons'] = array('strong', 'italic', 'underline');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the text editor XSS filter that will used for the given text format.
|
||||
*
|
||||
* Is only called when an EditorXssFilter will effectively be used; this hook
|
||||
* does not allow one to alter that decision.
|
||||
*
|
||||
* @param string &$editor_xss_filter_class
|
||||
* The text editor XSS filter class that will be used.
|
||||
* @param \Drupal\filter\FilterFormatInterface $format
|
||||
* The text format configuration entity. Provides context based upon which
|
||||
* one may want to adjust the filtering.
|
||||
* @param \Drupal\filter\FilterFormatInterface $original_format|null
|
||||
* (optional) The original text format configuration entity (when switching
|
||||
* text formats/editors). Also provides context based upon which one may want
|
||||
* to adjust the filtering.
|
||||
*
|
||||
* @see \Drupal\editor\EditorXssFilterInterface
|
||||
*/
|
||||
function hook_editor_xss_filter_alter(&$editor_xss_filter_class, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL) {
|
||||
$filters = $format->filters()->getAll();
|
||||
if (isset($filters['filter_wysiwyg']) && $filters['filter_wysiwyg']->status) {
|
||||
$editor_xss_filter_class = '\Drupal\filter_wysiwyg\EditorXssFilter\WysiwygFilter';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
10
core/modules/editor/editor.info.yml
Normal file
10
core/modules/editor/editor.info.yml
Normal file
|
@ -0,0 +1,10 @@
|
|||
name: 'Text Editor'
|
||||
type: module
|
||||
description: 'Provides a means to associate text formats with text editor libraries such as WYSIWYGs or toolbars.'
|
||||
package: Core
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- filter
|
||||
- file
|
||||
configure: filter.admin_overview
|
42
core/modules/editor/editor.libraries.yml
Normal file
42
core/modules/editor/editor.libraries.yml
Normal file
|
@ -0,0 +1,42 @@
|
|||
drupal.editor.admin:
|
||||
version: VERSION
|
||||
js:
|
||||
js/editor.admin.js: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/jquery.once
|
||||
- core/drupal
|
||||
|
||||
drupal.editor:
|
||||
version: VERSION
|
||||
js:
|
||||
js/editor.js: {}
|
||||
css:
|
||||
component:
|
||||
css/editor.css: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal
|
||||
- core/drupalSettings
|
||||
- core/jquery.once
|
||||
- core/drupal.dialog
|
||||
|
||||
drupal.editor.dialog:
|
||||
version: VERSION
|
||||
js:
|
||||
js/editor.dialog.js: {}
|
||||
dependencies:
|
||||
- core/jquery
|
||||
- core/drupal.dialog
|
||||
- core/drupal.ajax
|
||||
- core/drupalSettings
|
||||
|
||||
quickedit.inPlaceEditor.formattedText:
|
||||
version: VERSION
|
||||
js:
|
||||
js/editor.formattedTextEditor.js: { attributes: { defer: true } }
|
||||
dependencies:
|
||||
- quickedit/quickedit
|
||||
- editor/drupal.editor
|
||||
- core/drupal.ajax
|
||||
- core/drupalSettings
|
507
core/modules/editor/editor.module
Normal file
507
core/modules/editor/editor.module
Normal file
|
@ -0,0 +1,507 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Adds bindings for client-side "text editors" to text formats.
|
||||
*/
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Entity\FieldableEntityInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Drupal\filter\Plugin\FilterInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function editor_help($route_name, RouteMatchInterface $route_match) {
|
||||
switch ($route_name) {
|
||||
case 'help.page.editor':
|
||||
$output = '';
|
||||
$output .= '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . t('The Text Editor module provides a framework that other modules (such as <a href="!ckeditor">CKEditor module</a>) can use to provide toolbars and other functionality that allow users to format text more easily than typing HTML tags directly. For more information, see the <a href="!documentation">online documentation for the Text Editor module</a>.', array('!documentation' => 'https://www.drupal.org/documentation/modules/editor', '!ckeditor' => (\Drupal::moduleHandler()->moduleExists('ckeditor')) ? \Drupal::url('help.page', array('name' => 'ckeditor')) : '#')) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Installing text editors') . '</dt>';
|
||||
$output .= '<dd>' . t('The Text Editor module provides a framework for managing editors. To use it, you also need to enable a text editor. This can either be the core <a href="!ckeditor">CKEditor module</a>, which can be enabled on the <a href="!extend">Extend page</a>, or a contributed module for any other text editor. When installing a contributed text editor module, be sure to check the installation instructions, because you will most likely need to download and install an external library as well as the Drupal module.', array('!ckeditor' => (\Drupal::moduleHandler()->moduleExists('ckeditor')) ? \Drupal::url('help.page', array('name' => 'ckeditor')) : '#', '!extend' => \Drupal::url('system.modules_list'))) . '</dd>';
|
||||
$output .= '<dt>' . t('Enabling a text editor for a text format') . '</dt>';
|
||||
$output .= '<dd>' . t('On the <a href="!formats">Text formats and editors page</a> you can see which text editor is associated with each text format. You can change this by clicking on the <em>Configure</em> link, and then choosing a text editor or <em>none</em> from the <em>Text editor</em> drop-down list. The text editor will then be displayed with any text field for which this text format is chosen.', array('!formats' => \Drupal::url('filter.admin_overview'))) . '</dd>';
|
||||
$output .= '<dt>' . t('Configuring a text editor') . '</dt>';
|
||||
$output .= '<dd>' . t('Once a text editor is associated with a text format, you can configure it by clicking on the <em>Configure</em> link for this format. Depending on the specific text editor, you can configure it for example by adding buttons to its toolbar. Typically these buttons provide formatting or editing tools, and they often insert HTML tags into the field source. For details, see the help page of the specific text editor.') . '</dd>';
|
||||
$output .= '<dt>' . t('Using different text editors and formats') . '</dt>';
|
||||
$output .= '<dd>' . t('If you change the text format on a text field, the text editor will change as well because the text editor configuration is associated with the individual text format. This allows the use of the same text editor with different options for different text formats. It also allows users to choose between text formats with different text editors if they are installed.') . '</dd>';
|
||||
$output .= '</dl>';
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_menu_links_discovered_alter().
|
||||
*
|
||||
* Rewrites the menu entries for filter module that relate to the configuration
|
||||
* of text editors.
|
||||
*/
|
||||
function editor_menu_links_discovered_alter(array &$links) {
|
||||
$links['filter.admin_overview']['title'] = 'Text formats and editors';
|
||||
$links['filter.admin_overview']['description'] = 'Configure how user-contributed content is filtered and formatted, as well as the text editor user interface (WYSIWYGs or toolbars).';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_element_info_alter().
|
||||
*
|
||||
* Extends the functionality of text_format elements (provided by Filter
|
||||
* module), so that selecting a text format notifies a client-side text editor
|
||||
* when it should be enabled or disabled.
|
||||
*
|
||||
* @see filter_element_info()
|
||||
*/
|
||||
function editor_element_info_alter(&$types) {
|
||||
$types['text_format']['#pre_render'][] = 'element.editor:preRenderTextFormat';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*/
|
||||
function editor_form_filter_admin_overview_alter(&$form, FormStateInterface $form_state) {
|
||||
// @todo Cleanup column injection: https://www.drupal.org/node/1876718.
|
||||
// Splice in the column for "Text editor" into the header.
|
||||
$position = array_search('name', $form['formats']['#header']) + 1;
|
||||
$start = array_splice($form['formats']['#header'], 0, $position, array('editor' => t('Text editor')));
|
||||
$form['formats']['#header'] = array_merge($start, $form['formats']['#header']);
|
||||
|
||||
// Then splice in the name of each text editor for each text format.
|
||||
$editors = \Drupal::service('plugin.manager.editor')->getDefinitions();
|
||||
foreach (Element::children($form['formats']) as $format_id) {
|
||||
$editor = editor_load($format_id);
|
||||
$editor_name = ($editor && isset($editors[$editor->getEditor()])) ? $editors[$editor->getEditor()]['label'] : drupal_placeholder('—');
|
||||
$editor_column['editor'] = array('#markup' => $editor_name);
|
||||
$position = array_search('name', array_keys($form['formats'][$format_id])) + 1;
|
||||
$start = array_splice($form['formats'][$format_id], 0, $position, $editor_column);
|
||||
$form['formats'][$format_id] = array_merge($start, $form['formats'][$format_id]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_BASE_FORM_ID_alter() for 'filter_format_form'.
|
||||
*/
|
||||
function editor_form_filter_format_form_alter(&$form, FormStateInterface $form_state) {
|
||||
$editor = $form_state->get('editor');
|
||||
if ($editor === NULL) {
|
||||
$format = $form_state->getFormObject()->getEntity();
|
||||
$format_id = $format->isNew() ? NULL : $format->id();
|
||||
$editor = editor_load($format_id);
|
||||
$form_state->set('editor', $editor);
|
||||
}
|
||||
|
||||
// Associate a text editor with this text format.
|
||||
$manager = \Drupal::service('plugin.manager.editor');
|
||||
$editor_options = $manager->listOptions();
|
||||
$form['editor'] = array(
|
||||
// Position the editor selection before the filter settings (weight of 0),
|
||||
// but after the filter label and name (weight of -20).
|
||||
'#weight' => -9,
|
||||
);
|
||||
$form['editor']['editor'] = array(
|
||||
'#type' => 'select',
|
||||
'#title' => t('Text editor'),
|
||||
'#options' => $editor_options,
|
||||
'#empty_option' => t('None'),
|
||||
'#default_value' => $editor ? $editor->getEditor() : '',
|
||||
'#ajax' => array(
|
||||
'trigger_as' => array('name' => 'editor_configure'),
|
||||
'callback' => 'editor_form_filter_admin_form_ajax',
|
||||
'wrapper' => 'editor-settings-wrapper',
|
||||
),
|
||||
'#weight' => -10,
|
||||
);
|
||||
$form['editor']['configure'] = array(
|
||||
'#type' => 'submit',
|
||||
'#name' => 'editor_configure',
|
||||
'#value' => t('Configure'),
|
||||
'#limit_validation_errors' => array(array('editor')),
|
||||
'#submit' => array('editor_form_filter_admin_format_editor_configure'),
|
||||
'#ajax' => array(
|
||||
'callback' => 'editor_form_filter_admin_form_ajax',
|
||||
'wrapper' => 'editor-settings-wrapper',
|
||||
),
|
||||
'#weight' => -10,
|
||||
'#attributes' => array('class' => array('js-hide')),
|
||||
);
|
||||
|
||||
// If there aren't any options (other than "None"), disable the select list.
|
||||
if (empty($editor_options)) {
|
||||
$form['editor']['editor']['#disabled'] = TRUE;
|
||||
$form['editor']['editor']['#description'] = t('This option is disabled because no modules that provide a text editor are currently enabled.');
|
||||
}
|
||||
|
||||
$form['editor']['settings'] = array(
|
||||
'#tree' => TRUE,
|
||||
'#weight' => -8,
|
||||
'#type' => 'container',
|
||||
'#id' => 'editor-settings-wrapper',
|
||||
'#attached' => array(
|
||||
'library' => array(
|
||||
'editor/drupal.editor.admin',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Add editor-specific validation and submit handlers.
|
||||
if ($editor) {
|
||||
$plugin = $manager->createInstance($editor->getEditor());
|
||||
$settings_form = array();
|
||||
$settings_form['#element_validate'][] = array($plugin, 'settingsFormValidate');
|
||||
$form['editor']['settings']['subform'] = $plugin->settingsForm($settings_form, $form_state, $editor);
|
||||
$form['editor']['settings']['subform']['#parents'] = array('editor', 'settings');
|
||||
$form['actions']['submit']['#submit'][] = array($plugin, 'settingsFormSubmit');
|
||||
}
|
||||
|
||||
$form['#validate'][] = 'editor_form_filter_admin_format_validate';
|
||||
$form['actions']['submit']['#submit'][] = 'editor_form_filter_admin_format_submit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Button submit handler for filter_format_form()'s 'editor_configure' button.
|
||||
*/
|
||||
function editor_form_filter_admin_format_editor_configure($form, FormStateInterface $form_state) {
|
||||
$editor = $form_state->get('editor');
|
||||
$editor_value = $form_state->getValue(array('editor', 'editor'));
|
||||
if ($editor_value !== NULL) {
|
||||
if ($editor_value === '') {
|
||||
$form_state->set('editor', FALSE);
|
||||
}
|
||||
elseif (empty($editor) || $editor_value !== $editor->getEditor()) {
|
||||
$format = $form_state->getFormObject()->getEntity();
|
||||
$editor = entity_create('editor', array(
|
||||
'format' => $format->isNew() ? NULL : $format->id(),
|
||||
'editor' => $editor_value,
|
||||
));
|
||||
$form_state->set('editor', $editor);
|
||||
}
|
||||
}
|
||||
$form_state->setRebuild();
|
||||
}
|
||||
|
||||
/**
|
||||
* AJAX callback handler for filter_format_form().
|
||||
*/
|
||||
function editor_form_filter_admin_form_ajax($form, FormStateInterface $form_state) {
|
||||
return $form['editor']['settings'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional validate handler for filter_format_form().
|
||||
*/
|
||||
function editor_form_filter_admin_format_validate($form, FormStateInterface $form_state) {
|
||||
// This validate handler is not applicable when using the 'Configure' button.
|
||||
if ($form_state->getTriggeringElement()['#name'] === 'editor_configure') {
|
||||
return;
|
||||
}
|
||||
|
||||
// When using this form with JavaScript disabled in the browser, the
|
||||
// 'Configure' button won't be clicked automatically. So, when the user has
|
||||
// selected a text editor and has then clicked 'Save configuration', we should
|
||||
// point out that the user must still configure the text editor.
|
||||
if ($form_state->getValue(['editor', 'editor']) !== '' && !$form_state->get('editor')) {
|
||||
$form_state->setErrorByName('editor][editor', t('You must configure the selected text editor.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional submit handler for filter_format_form().
|
||||
*/
|
||||
function editor_form_filter_admin_format_submit($form, FormStateInterface $form_state) {
|
||||
// Delete the existing editor if disabling or switching between editors.
|
||||
$format = $form_state->getFormObject()->getEntity();
|
||||
$format_id = $format->isNew() ? NULL : $format->id();
|
||||
$original_editor = editor_load($format_id);
|
||||
if ($original_editor && $original_editor->getEditor() != $form_state->getValue(array('editor', 'editor'))) {
|
||||
$original_editor->delete();
|
||||
}
|
||||
|
||||
// Create a new editor or update the existing editor.
|
||||
if ($editor = $form_state->get('editor')) {
|
||||
// Ensure the text format is set: when creating a new text format, this
|
||||
// would equal the empty string.
|
||||
$editor->set('format', $format_id);
|
||||
$editor->setSettings($form_state->getValue(['editor', 'settings']));
|
||||
$editor->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads an individual configured text editor based on text format ID.
|
||||
*
|
||||
* @return \Drupal\editor\Entity\Editor|null
|
||||
* A text editor object, or NULL.
|
||||
*/
|
||||
function editor_load($format_id) {
|
||||
// Load all the editors at once here, assuming that either no editors or more
|
||||
// than one editor will be needed on a page (such as having multiple text
|
||||
// formats for administrators). Loading a small number of editors all at once
|
||||
// is more efficient than loading multiple editors individually.
|
||||
$editors = entity_load_multiple('editor');
|
||||
return isset($editors[$format_id]) ? $editors[$format_id] : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies text editor XSS filtering.
|
||||
*
|
||||
* @param string $html
|
||||
* The HTML string that will be passed to the text editor.
|
||||
* @param \Drupal\filter\FilterFormatInterface $format
|
||||
* The text format whose text editor will be used.
|
||||
* @param \Drupal\filter\FilterFormatInterface $original_format|null
|
||||
* (optional) The original text format (i.e. when switching text formats,
|
||||
* $format is the text format that is going to be used, $original_format is
|
||||
* the one that was being used initially, the one that is stored in the
|
||||
* database when editing).
|
||||
*
|
||||
* @return string|false
|
||||
* FALSE when no XSS filtering needs to be applied (either because no text
|
||||
* editor is associated with the text format, or because the text editor is
|
||||
* safe from XSS attacks, or because the text format does not use any XSS
|
||||
* protection filters), otherwise the XSS filtered string.
|
||||
*
|
||||
* @see https://www.drupal.org/node/2099741
|
||||
*/
|
||||
function editor_filter_xss($html, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL) {
|
||||
$editor = editor_load($format->id());
|
||||
|
||||
// If no text editor is associated with this text format, then we don't need
|
||||
// text editor XSS filtering either.
|
||||
if (!isset($editor)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// If the text editor associated with this text format guarantees security,
|
||||
// then we also don't need text editor XSS filtering.
|
||||
$definition = \Drupal::service('plugin.manager.editor')->getDefinition($editor->getEditor());
|
||||
if ($definition['is_xss_safe'] === TRUE) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// If there is no filter preventing XSS attacks in the text format being used,
|
||||
// then no text editor XSS filtering is needed either. (Because then the
|
||||
// editing user can already be attacked by merely viewing the content.)
|
||||
// e.g.: an admin user creates content in Full HTML and then edits it, no text
|
||||
// format switching happens; in this case, no text editor XSS filtering is
|
||||
// desirable, because it would strip style attributes, amongst others.
|
||||
$current_filter_types = $format->getFilterTypes();
|
||||
if (!in_array(FilterInterface::TYPE_HTML_RESTRICTOR, $current_filter_types, TRUE)) {
|
||||
if ($original_format === NULL) {
|
||||
return FALSE;
|
||||
}
|
||||
// Unless we are switching from another text format, in which case we must
|
||||
// first check whether a filter preventing XSS attacks is used in that text
|
||||
// format, and if so, we must still apply XSS filtering.
|
||||
// e.g.: an anonymous user creates content in Restricted HTML, an admin user
|
||||
// edits it (then no XSS filtering is applied because no text editor is
|
||||
// used), and switches to Full HTML (for which a text editor is used). Then
|
||||
// we must apply XSS filtering to protect the admin user.
|
||||
else {
|
||||
$original_filter_types = $original_format->getFilterTypes();
|
||||
if (!in_array(FilterInterface::TYPE_HTML_RESTRICTOR, $original_filter_types, TRUE)) {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise, apply the text editor XSS filter. We use the default one unless
|
||||
// a module tells us to use a different one.
|
||||
$editor_xss_filter_class = '\Drupal\editor\EditorXssFilter\Standard';
|
||||
\Drupal::moduleHandler()->alter('editor_xss_filter', $editor_xss_filter_class, $format, $original_format);
|
||||
|
||||
return call_user_func($editor_xss_filter_class . '::filterXss', $html, $format, $original_format);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_insert().
|
||||
*/
|
||||
function editor_entity_insert(EntityInterface $entity) {
|
||||
// Only act on content entities.
|
||||
if (!($entity instanceof FieldableEntityInterface)) {
|
||||
return;
|
||||
}
|
||||
$referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
|
||||
foreach ($referenced_files_by_field as $field => $uuids) {
|
||||
_editor_record_file_usage($uuids, $entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_update().
|
||||
*/
|
||||
function editor_entity_update(EntityInterface $entity) {
|
||||
// Only act on content entities.
|
||||
if (!($entity instanceof FieldableEntityInterface)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// On new revisions, all files are considered to be a new usage and no
|
||||
// deletion of previous file usages are necessary.
|
||||
if (!empty($entity->original) && $entity->getRevisionId() != $entity->original->getRevisionId()) {
|
||||
$referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
|
||||
foreach ($referenced_files_by_field as $field => $uuids) {
|
||||
_editor_record_file_usage($uuids, $entity);
|
||||
}
|
||||
}
|
||||
// On modified revisions, detect which file references have been added (and
|
||||
// record their usage) and which ones have been removed (delete their usage).
|
||||
// File references that existed both in the previous version of the revision
|
||||
// and in the new one don't need their usage to be updated.
|
||||
else {
|
||||
$original_uuids_by_field = _editor_get_file_uuids_by_field($entity->original);
|
||||
$uuids_by_field = _editor_get_file_uuids_by_field($entity);
|
||||
|
||||
// Detect file usages that should be incremented.
|
||||
foreach ($uuids_by_field as $field => $uuids) {
|
||||
$added_files = array_diff($uuids_by_field[$field], $original_uuids_by_field[$field]);
|
||||
_editor_record_file_usage($added_files, $entity);
|
||||
}
|
||||
|
||||
// Detect file usages that should be decremented.
|
||||
foreach ($original_uuids_by_field as $field => $uuids) {
|
||||
$removed_files = array_diff($original_uuids_by_field[$field], $uuids_by_field[$field]);
|
||||
_editor_delete_file_usage($removed_files, $entity, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_delete().
|
||||
*/
|
||||
function editor_entity_delete(EntityInterface $entity) {
|
||||
// Only act on content entities.
|
||||
if (!($entity instanceof FieldableEntityInterface)) {
|
||||
return;
|
||||
}
|
||||
$referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
|
||||
foreach ($referenced_files_by_field as $field => $uuids) {
|
||||
_editor_delete_file_usage($uuids, $entity, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_revision_delete().
|
||||
*/
|
||||
function editor_entity_revision_delete(EntityInterface $entity) {
|
||||
// Only act on content entities.
|
||||
if (!($entity instanceof FieldableEntityInterface)) {
|
||||
return;
|
||||
}
|
||||
$referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
|
||||
foreach ($referenced_files_by_field as $field => $uuids) {
|
||||
_editor_delete_file_usage($uuids, $entity, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records file usage of files referenced by formatted text fields.
|
||||
*
|
||||
* Every referenced file that does not yet have the FILE_STATUS_PERMANENT state,
|
||||
* will be given that state.
|
||||
*
|
||||
* @param array $uuids
|
||||
* An array of file entity UUIDs.
|
||||
* @param EntityInterface $entity
|
||||
* An entity whose fields to inspect for file references.
|
||||
*/
|
||||
function _editor_record_file_usage(array $uuids, EntityInterface $entity) {
|
||||
foreach ($uuids as $uuid) {
|
||||
if ($file = \Drupal::entityManager()->loadEntityByUuid('file', $uuid)) {
|
||||
if ($file->status !== FILE_STATUS_PERMANENT) {
|
||||
$file->status = FILE_STATUS_PERMANENT;
|
||||
$file->save();
|
||||
}
|
||||
\Drupal::service('file.usage')->add($file, 'editor', $entity->getEntityTypeId(), $entity->id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes file usage of files referenced by formatted text fields.
|
||||
*
|
||||
* @param array $uuids
|
||||
* An array of file entity UUIDs.
|
||||
* @param EntityInterface $entity
|
||||
* An entity whose fields to inspect for file references.
|
||||
* @param $count
|
||||
* The number of references to delete. Should be 1 when deleting a single
|
||||
* revision and 0 when deleting an entity entirely.
|
||||
*
|
||||
* @see \Drupal\file\FileUsage\FileUsageInterface::delete()
|
||||
*/
|
||||
function _editor_delete_file_usage(array $uuids, EntityInterface $entity, $count) {
|
||||
foreach ($uuids as $uuid) {
|
||||
if ($file = \Drupal::entityManager()->loadEntityByUuid('file', $uuid)) {
|
||||
\Drupal::service('file.usage')->delete($file, 'editor', $entity->getEntityTypeId(), $entity->id(), $count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds all files referenced (data-entity-uuid) by formatted text fields.
|
||||
*
|
||||
* @param EntityInterface $entity
|
||||
* An entity whose fields to analyze.
|
||||
*
|
||||
* @return array
|
||||
* An array of file entity UUIDs.
|
||||
*/
|
||||
function _editor_get_file_uuids_by_field(EntityInterface $entity) {
|
||||
$uuids = array();
|
||||
|
||||
$formatted_text_fields = _editor_get_formatted_text_fields($entity);
|
||||
foreach ($formatted_text_fields as $formatted_text_field) {
|
||||
$text = $entity->get($formatted_text_field)->value;
|
||||
$uuids[$formatted_text_field] = _editor_parse_file_uuids($text);
|
||||
}
|
||||
return $uuids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the formatted text fields on an entity.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\FieldableEntityInterface $entity
|
||||
* An entity whose fields to analyze.
|
||||
*
|
||||
* @return array
|
||||
* The names of the fields on this entity that support formatted text.
|
||||
*/
|
||||
function _editor_get_formatted_text_fields(FieldableEntityInterface $entity) {
|
||||
$field_definitions = $entity->getFieldDefinitions();
|
||||
if (empty($field_definitions)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Only return formatted text fields.
|
||||
return array_keys(array_filter($field_definitions, function (FieldDefinitionInterface $definition) {
|
||||
return in_array($definition->getType(), array('text', 'text_long', 'text_with_summary'), TRUE);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an HTML snippet for any linked file with data-entity-uuid attributes.
|
||||
*
|
||||
* @param string $text
|
||||
* The partial (X)HTML snippet to load. Invalid markup will be corrected on
|
||||
* import.
|
||||
*
|
||||
* @return array
|
||||
* An array of all found UUIDs.
|
||||
*/
|
||||
function _editor_parse_file_uuids($text) {
|
||||
$dom = Html::load($text);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$uuids = array();
|
||||
foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid]') as $node) {
|
||||
$uuids[] = $node->getAttribute('data-entity-uuid');
|
||||
}
|
||||
return $uuids;
|
||||
}
|
35
core/modules/editor/editor.routing.yml
Normal file
35
core/modules/editor/editor.routing.yml
Normal file
|
@ -0,0 +1,35 @@
|
|||
editor.filter_xss:
|
||||
path: '/editor/filter_xss/{filter_format}'
|
||||
defaults:
|
||||
_controller: '\Drupal\editor\EditorController::filterXss'
|
||||
requirements:
|
||||
_entity_access: 'filter_format.use'
|
||||
|
||||
editor.field_untransformed_text:
|
||||
path: '/editor/{entity_type}/{entity}/{field_name}/{langcode}/{view_mode_id}'
|
||||
defaults:
|
||||
_controller: '\Drupal\editor\EditorController::getUntransformedText'
|
||||
options:
|
||||
_theme: ajax_base_page
|
||||
parameters:
|
||||
entity:
|
||||
type: entity:{entity_type}
|
||||
requirements:
|
||||
_permission: 'access in-place editing'
|
||||
_access_quickedit_entity_field: 'TRUE'
|
||||
|
||||
editor.image_dialog:
|
||||
path: '/editor/dialog/image/{filter_format}'
|
||||
defaults:
|
||||
_form: '\Drupal\editor\Form\EditorImageDialog'
|
||||
_title: 'Upload image'
|
||||
requirements:
|
||||
_entity_access: 'filter_format.use'
|
||||
|
||||
editor.link_dialog:
|
||||
path: '/editor/dialog/link/{filter_format}'
|
||||
defaults:
|
||||
_form: '\Drupal\editor\Form\EditorLinkDialog'
|
||||
_title: 'Add link'
|
||||
requirements:
|
||||
_entity_access: 'filter_format.use'
|
7
core/modules/editor/editor.services.yml
Normal file
7
core/modules/editor/editor.services.yml
Normal file
|
@ -0,0 +1,7 @@
|
|||
services:
|
||||
plugin.manager.editor:
|
||||
class: Drupal\editor\Plugin\EditorManager
|
||||
parent: default_plugin_manager
|
||||
element.editor:
|
||||
class: Drupal\editor\Element
|
||||
arguments: ['@plugin.manager.editor']
|
877
core/modules/editor/js/editor.admin.js
Normal file
877
core/modules/editor/js/editor.admin.js
Normal file
|
@ -0,0 +1,877 @@
|
|||
/**
|
||||
* @file
|
||||
* Provides a JavaScript API to broadcast text editor configuration changes.
|
||||
*
|
||||
* Filter implementations may listen to the drupalEditorFeatureAdded,
|
||||
* drupalEditorFeatureRemoved, and drupalEditorFeatureRemoved events on document
|
||||
* to automatically adjust their settings based on the editor configuration.
|
||||
*/
|
||||
|
||||
(function ($, _, Drupal, document) {
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.editorConfiguration = {
|
||||
|
||||
/**
|
||||
* Must be called by a specific text editor's configuration whenever a
|
||||
* feature is added by the user.
|
||||
*
|
||||
* Triggers the drupalEditorFeatureAdded event on the document, which
|
||||
* receives a {@link Drupal.EditorFeature} object.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @fires event:drupalEditorFeatureAdded
|
||||
*/
|
||||
addedFeature: function (feature) {
|
||||
$(document).trigger('drupalEditorFeatureAdded', feature);
|
||||
},
|
||||
|
||||
/**
|
||||
* Must be called by a specific text editor's configuration whenever a
|
||||
* feature is removed by the user.
|
||||
*
|
||||
* Triggers the drupalEditorFeatureRemoved event on the document, which
|
||||
* receives a {@link Drupal.EditorFeature} object.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @fires event:drupalEditorFeatureRemoved
|
||||
*/
|
||||
removedFeature: function (feature) {
|
||||
$(document).trigger('drupalEditorFeatureRemoved', feature);
|
||||
},
|
||||
|
||||
/**
|
||||
* Must be called by a specific text editor's configuration whenever a
|
||||
* feature is modified, i.e. has different rules.
|
||||
*
|
||||
* For example when the "Bold" button is configured to use the `<b>` tag
|
||||
* instead of the `<strong>` tag.
|
||||
*
|
||||
* Triggers the drupalEditorFeatureModified event on the document, which
|
||||
* receives a {@link Drupal.EditorFeature} object.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @fires event:drupalEditorFeatureModified
|
||||
*/
|
||||
modifiedFeature: function (feature) {
|
||||
$(document).trigger('drupalEditorFeatureModified', feature);
|
||||
},
|
||||
|
||||
/**
|
||||
* May be called by a specific text editor's configuration whenever a
|
||||
* feature is being added, to check whether it would require the filter
|
||||
* settings to be updated.
|
||||
*
|
||||
* The canonical use case is when a text editor is being enabled:
|
||||
* preferably
|
||||
* this would not cause the filter settings to be changed; rather, the
|
||||
* default set of buttons (features) for the text editor should adjust
|
||||
* itself to not cause filter setting changes.
|
||||
*
|
||||
* Note: for filters to integrate with this functionality, it is necessary
|
||||
* that they implement
|
||||
* `Drupal.filterSettingsForEditors[filterID].getRules()`.
|
||||
*
|
||||
* @param {Drupal.EditorFeature} feature
|
||||
* A text editor feature object.
|
||||
*
|
||||
* @return {bool}
|
||||
* Whether the given feature is allowed by the current filters.
|
||||
*/
|
||||
featureIsAllowedByFilters: function (feature) {
|
||||
|
||||
/**
|
||||
* Generate the universe U of possible values that can result from the
|
||||
* feature's rules' requirements.
|
||||
*
|
||||
* This generates an object of this form:
|
||||
* var universe = {
|
||||
* a: {
|
||||
* 'touchedByAllowedPropertyRule': false,
|
||||
* 'tag': false,
|
||||
* 'attributes:href': false,
|
||||
* 'classes:external': false,
|
||||
* },
|
||||
* strong: {
|
||||
* 'touchedByAllowedPropertyRule': false,
|
||||
* 'tag': false,
|
||||
* },
|
||||
* img: {
|
||||
* 'touchedByAllowedPropertyRule': false,
|
||||
* 'tag': false,
|
||||
* 'attributes:src': false
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* In this example, the given text editor feature resulted in the above
|
||||
* universe, which shows that it must be allowed to generate the a,
|
||||
* strong and img tags. For the a tag, it must be able to set the "href"
|
||||
* attribute and the "external" class. For the strong tag, no further
|
||||
* properties are required. For the img tag, the "src" attribute is
|
||||
* required. The "tag" key is used to track whether that tag was
|
||||
* explicitly allowed by one of the filter's rules. The
|
||||
* "touchedByAllowedPropertyRule" key is used for state tracking that is
|
||||
* essential for filterStatusAllowsFeature() to be able to reason: when
|
||||
* all of a filter's rules have been applied, and none of the forbidden
|
||||
* rules matched (which would have resulted in early termination) yet the
|
||||
* universe has not been made empty (which would be the end result if
|
||||
* everything in the universe were explicitly allowed), then this piece
|
||||
* of state data enables us to determine whether a tag whose properties
|
||||
* were not all explicitly allowed are in fact still allowed, because its
|
||||
* tag was explicitly allowed and there were no filter rules applying
|
||||
* "allowed tag property value" restrictions for this particular tag.
|
||||
*
|
||||
* @param {object} feature
|
||||
*
|
||||
* @return {object}
|
||||
*
|
||||
* @see findPropertyValueOnTag()
|
||||
* @see filterStatusAllowsFeature()
|
||||
*/
|
||||
function generateUniverseFromFeatureRequirements(feature) {
|
||||
var properties = ['attributes', 'styles', 'classes'];
|
||||
var universe = {};
|
||||
|
||||
for (var r = 0; r < feature.rules.length; r++) {
|
||||
var featureRule = feature.rules[r];
|
||||
|
||||
// For each tag required by this feature rule, create a basic entry in
|
||||
// the universe.
|
||||
var requiredTags = featureRule.required.tags;
|
||||
for (var t = 0; t < requiredTags.length; t++) {
|
||||
universe[requiredTags[t]] = {
|
||||
// Whether this tag was allowed or not.
|
||||
tag: false,
|
||||
// Whether any filter rule that applies to this tag had an allowed
|
||||
// property rule. i.e. will become true if >=1 filter rule has >=1
|
||||
// allowed property rule.
|
||||
touchedByAllowedPropertyRule: false,
|
||||
// Analogous, but for forbidden property rule.
|
||||
touchedBytouchedByForbiddenPropertyRule: false
|
||||
};
|
||||
}
|
||||
|
||||
// If no required properties are defined for this rule, we can move on
|
||||
// to the next feature.
|
||||
if (emptyProperties(featureRule.required)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Expand the existing universe, assume that each tags' property
|
||||
// value is disallowed. If the filter rules allow everything in the
|
||||
// feature's universe, then the feature is allowed.
|
||||
for (var p = 0; p < properties.length; p++) {
|
||||
var property = properties[p];
|
||||
for (var pv = 0; pv < featureRule.required[property].length; pv++) {
|
||||
var propertyValue = featureRule.required[property];
|
||||
universe[requiredTags][property + ':' + propertyValue] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return universe;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provided a section of a feature or filter rule, checks if no property
|
||||
* values are defined for all properties: attributes, classes and styles.
|
||||
*
|
||||
* @param {object} section
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
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
|
||||
* @param {string} tag
|
||||
* @param {string} property
|
||||
* @param {Array} propertyValues
|
||||
* @param {bool} allowing
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
function findPropertyValuesOnTag(universe, tag, property, propertyValues, allowing) {
|
||||
// Detect the wildcard case.
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls findPropertyValuesOnAllTags for all tags in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* @param {string} property
|
||||
* @param {Array} propertyValues
|
||||
* @param {bool} allowing
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds out if a specific property value (potentially containing
|
||||
* wildcards) exists on the given tag. When the "allowing" parameter
|
||||
* equals true, the universe will be updated if that specific property
|
||||
* value exists. Returns true if found, false otherwise.
|
||||
*
|
||||
* @param {object} universe
|
||||
* @param {string} tag
|
||||
* @param {string} property
|
||||
* @param {string} propertyValue
|
||||
* @param {bool} allowing
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
function findPropertyValueOnTag(universe, tag, property, propertyValue, allowing) {
|
||||
// If the tag does not exist in the universe, then it definitely can't
|
||||
// have this specific property value.
|
||||
if (!_.has(universe, tag)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
var key = property + ':' + propertyValue;
|
||||
|
||||
// Track whether a tag was touched by a filter rule that allows specific
|
||||
// property values on this particular tag.
|
||||
// @see generateUniverseFromFeatureRequirements
|
||||
if (allowing) {
|
||||
universe[tag].touchedByAllowedPropertyRule = true;
|
||||
}
|
||||
|
||||
// The simple case: no wildcard in property value.
|
||||
if (_.indexOf(propertyValue, '*') === -1) {
|
||||
if (_.has(universe, tag) && _.has(universe[tag], key)) {
|
||||
if (allowing) {
|
||||
universe[tag][key] = true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
// The complex case: wildcard in property value.
|
||||
else {
|
||||
var atLeastOneFound = false;
|
||||
var regex = key.replace(/\*/g, "[^ ]*");
|
||||
_.each(_.keys(universe[tag]), function (key) {
|
||||
if (key.match(regex)) {
|
||||
atLeastOneFound = true;
|
||||
if (allowing) {
|
||||
universe[tag][key] = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
return atLeastOneFound;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a tag from the universe if the tag itself and each of its
|
||||
* properties are marked as allowed.
|
||||
*
|
||||
* @param {object} universe
|
||||
* @param {string} tag
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
function deleteFromUniverseIfAllowed(universe, tag) {
|
||||
// Detect the wildcard case.
|
||||
if (tag === '*') {
|
||||
return deleteAllTagsFromUniverseIfAllowed(universe);
|
||||
}
|
||||
if (_.has(universe, tag) && _.every(_.omit(universe[tag], 'touchedByAllowedPropertyRule'))) {
|
||||
delete universe[tag];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls deleteFromUniverseIfAllowed for all tags in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
function deleteAllTagsFromUniverseIfAllowed(universe) {
|
||||
var atLeastOneDeleted = false;
|
||||
_.each(_.keys(universe), function (tag) {
|
||||
if (deleteFromUniverseIfAllowed(universe, tag)) {
|
||||
atLeastOneDeleted = true;
|
||||
}
|
||||
});
|
||||
return atLeastOneDeleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any filter rule forbids either a tag or a tag property value
|
||||
* that exists in the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* @param {object} filterStatus
|
||||
*
|
||||
* @return {bool}
|
||||
*/
|
||||
function anyForbiddenFilterRuleMatches(universe, filterStatus) {
|
||||
var properties = ['attributes', 'styles', 'classes'];
|
||||
|
||||
// Check if a tag in the universe is forbidden.
|
||||
var allRequiredTags = _.keys(universe);
|
||||
var filterRule;
|
||||
for (var i = 0; i < filterStatus.rules.length; i++) {
|
||||
filterRule = filterStatus.rules[i];
|
||||
if (filterRule.allow === false) {
|
||||
if (_.intersection(allRequiredTags, filterRule.tags).length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a property value of a tag in the universe is forbidden.
|
||||
// For all filter rules…
|
||||
for (var n = 0; n < filterStatus.rules.length; n++) {
|
||||
filterRule = filterStatus.rules[n];
|
||||
// … if there are tags with restricted property values …
|
||||
if (filterRule.restrictedTags.tags.length && !emptyProperties(filterRule.restrictedTags.forbidden)) {
|
||||
// … for all those tags …
|
||||
for (var j = 0; j < filterRule.restrictedTags.tags.length; j++) {
|
||||
var tag = filterRule.restrictedTags.tags[j];
|
||||
// … then iterate over all properties …
|
||||
for (var k = 0; k < properties.length; k++) {
|
||||
var property = properties[k];
|
||||
// … and return true if just one of the forbidden property
|
||||
// values for this tag and property is listed in the universe.
|
||||
if (findPropertyValuesOnTag(universe, tag, property, filterRule.restrictedTags.forbidden[property], false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies every filter rule's explicit allowing of a tag or a tag
|
||||
* property value to the universe. Whenever both the tag and all of its
|
||||
* required property values are marked as explicitly allowed, they are
|
||||
* deleted from the universe.
|
||||
*
|
||||
* @param {object} universe
|
||||
* @param {object} filterStatus
|
||||
*/
|
||||
function markAllowedTagsAndPropertyValues(universe, filterStatus) {
|
||||
var properties = ['attributes', 'styles', 'classes'];
|
||||
|
||||
// Check if a tag in the universe is allowed.
|
||||
var filterRule;
|
||||
var tag;
|
||||
for (var l = 0; !_.isEmpty(universe) && l < filterStatus.rules.length; l++) {
|
||||
filterRule = filterStatus.rules[l];
|
||||
if (filterRule.allow === true) {
|
||||
for (var m = 0; !_.isEmpty(universe) && m < filterRule.tags.length; m++) {
|
||||
tag = filterRule.tags[m];
|
||||
if (_.has(universe, tag)) {
|
||||
universe[tag].tag = true;
|
||||
deleteFromUniverseIfAllowed(universe, tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a property value of a tag in the universe is allowed.
|
||||
// For all filter rules…
|
||||
for (var i = 0; !_.isEmpty(universe) && i < filterStatus.rules.length; i++) {
|
||||
filterRule = filterStatus.rules[i];
|
||||
// … if there are tags with restricted property values …
|
||||
if (filterRule.restrictedTags.tags.length && !emptyProperties(filterRule.restrictedTags.allowed)) {
|
||||
// … for all those tags …
|
||||
for (var j = 0; !_.isEmpty(universe) && j < filterRule.restrictedTags.tags.length; j++) {
|
||||
tag = filterRule.restrictedTags.tags[j];
|
||||
// … then iterate over all properties …
|
||||
for (var k = 0; k < properties.length; k++) {
|
||||
var property = properties[k];
|
||||
// … and try to delete this tag from the universe if just one
|
||||
// of the allowed property values for this tag and property is
|
||||
// listed in the universe. (Because everything might be allowed
|
||||
// now.)
|
||||
if (findPropertyValuesOnTag(universe, tag, property, filterRule.restrictedTags.allowed[property], true)) {
|
||||
deleteFromUniverseIfAllowed(universe, tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the current status of a filter allows a specific feature
|
||||
* by building the universe of potential values from the feature's
|
||||
* requirements and then checking whether anything in the filter prevents
|
||||
* that.
|
||||
*
|
||||
* @param {object} filterStatus
|
||||
* @param {object} feature
|
||||
*
|
||||
* @return {bool}
|
||||
*
|
||||
* @see generateUniverseFromFeatureRequirements()
|
||||
*/
|
||||
function filterStatusAllowsFeature(filterStatus, feature) {
|
||||
// An inactive filter by definition allows the feature.
|
||||
if (!filterStatus.active) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A feature that specifies no rules has no HTML requirements and is
|
||||
// hence allowed by definition.
|
||||
if (feature.rules.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Analogously for a filter that specifies no rules.
|
||||
if (filterStatus.rules.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Generate the universe U of possible values that can result from the
|
||||
// feature's rules' requirements.
|
||||
var universe = generateUniverseFromFeatureRequirements(feature);
|
||||
|
||||
// If anything that is in the universe (and is thus required by the
|
||||
// feature) is forbidden by any of the filter's rules, then this filter
|
||||
// does not allow this feature.
|
||||
if (anyForbiddenFilterRuleMatches(universe, filterStatus)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Mark anything in the universe that is allowed by any of the filter's
|
||||
// rules as allowed. If everything is explicitly allowed, then the
|
||||
// universe will become empty.
|
||||
markAllowedTagsAndPropertyValues(universe, filterStatus);
|
||||
|
||||
// If there was at least one filter rule allowing tags, then everything
|
||||
// in the universe must be allowed for this feature to be allowed, and
|
||||
// thus by now it must be empty. However, it is still possible that the
|
||||
// filter allows the feature, due to no rules for allowing tag property
|
||||
// values and/or rules for forbidding tag property values. For details:
|
||||
// see the comments below.
|
||||
// @see generateUniverseFromFeatureRequirements()
|
||||
if (_.some(_.pluck(filterStatus.rules, 'allow'))) {
|
||||
// If the universe is empty, then everything was explicitly allowed
|
||||
// and our job is done: this filter allows this feature!
|
||||
if (_.isEmpty(universe)) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise, it is still possible that this feature is allowed.
|
||||
else {
|
||||
// Every tag must be explicitly allowed if there are filter rules
|
||||
// doing tag whitelisting.
|
||||
if (!_.every(_.pluck(universe, 'tag'))) {
|
||||
return false;
|
||||
}
|
||||
// Every tag was explicitly allowed, but since the universe is not
|
||||
// empty, one or more tag properties are disallowed. However, if
|
||||
// only blacklisting of tag properties was applied to these tags,
|
||||
// and no whitelisting was ever applied, then it's still fine:
|
||||
// since none of the tag properties were blacklisted, we got to
|
||||
// this point, and since no whitelisting was applied, it doesn't
|
||||
// matter that the properties: this could never have happened
|
||||
// anyway. It's only this late that we can know this for certain.
|
||||
else {
|
||||
var tags = _.keys(universe);
|
||||
// Figure out if there was any rule applying whitelisting tag
|
||||
// restrictions to each of the remaining tags.
|
||||
for (var i = 0; i < tags.length; i++) {
|
||||
var tag = tags[i];
|
||||
if (_.has(universe, tag)) {
|
||||
if (universe[tag].touchedByAllowedPropertyRule === false) {
|
||||
delete universe[tag];
|
||||
}
|
||||
}
|
||||
}
|
||||
return _.isEmpty(universe);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Otherwise, if all filter rules were doing blacklisting, then the sole
|
||||
// fact that we got to this point indicates that this filter allows for
|
||||
// everything that is required for this feature.
|
||||
else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// If any filter's current status forbids the editor feature, return
|
||||
// false.
|
||||
Drupal.filterConfiguration.update();
|
||||
for (var filterID in Drupal.filterConfiguration.statuses) {
|
||||
if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) {
|
||||
var filterStatus = Drupal.filterConfiguration.statuses[filterID];
|
||||
if (!(filterStatusAllowsFeature(filterStatus, feature))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Constructor for an editor feature HTML rule.
|
||||
*
|
||||
* Intended to be used in combination with {@link Drupal.EditorFeature}.
|
||||
*
|
||||
* A text editor feature rule object describes both:
|
||||
* - required HTML tags, attributes, styles and classes: without these, the
|
||||
* text editor feature is unable to function. It's possible that a
|
||||
* - allowed HTML tags, attributes, styles and classes: these are optional
|
||||
* in the strictest sense, but it is possible that the feature generates
|
||||
* them.
|
||||
*
|
||||
* The structure can be very clearly seen below: there's a "required" and an
|
||||
* "allowed" key. For each of those, there are objects with the "tags",
|
||||
* "attributes", "styles" and "classes" keys. For all these keys the values
|
||||
* are initialized to the empty array. List each possible value as an array
|
||||
* value. Besides the "required" and "allowed" keys, there's an optional
|
||||
* "raw" key: it allows text editor implementations to optionally pass in
|
||||
* their raw representation instead of the Drupal-defined representation for
|
||||
* HTML rules.
|
||||
*
|
||||
* @example
|
||||
* tags: ['<a>']
|
||||
* attributes: ['href', 'alt']
|
||||
* styles: ['color', 'text-decoration']
|
||||
* classes: ['external', 'internal']
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @see Drupal.EditorFeature
|
||||
*/
|
||||
Drupal.EditorFeatureHTMLRule = function () {
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {Array} tags
|
||||
* @prop {Array} attributes
|
||||
* @prop {Array} styles
|
||||
* @prop {Array} classes
|
||||
*/
|
||||
this.required = {tags: [], attributes: [], styles: [], classes: []};
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {object}
|
||||
*
|
||||
* @prop {Array} tags
|
||||
* @prop {Array} attributes
|
||||
* @prop {Array} styles
|
||||
* @prop {Array} classes
|
||||
*/
|
||||
this.allowed = {tags: [], attributes: [], styles: [], classes: []};
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {null}
|
||||
*/
|
||||
this.raw = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* A text editor feature object. Initialized with the feature name.
|
||||
*
|
||||
* Contains a set of HTML rules ({@link Drupal.EditorFeatureHTMLRule} objects)
|
||||
* that describe which HTML tags, attributes, styles and classes are required
|
||||
* (i.e. essential for the feature to function at all) and which are allowed
|
||||
* (i.e. the feature may generate this, but they're not essential).
|
||||
*
|
||||
* It is necessary to allow for multiple HTML rules per feature: with just
|
||||
* one HTML rule per feature, there is not enough expressiveness to describe
|
||||
* certain cases. For example: a "table" feature would probably require the
|
||||
* `<table>` tag, and might allow e.g. the "summary" attribute on that tag.
|
||||
* However, the table feature would also require the `<tr>` and `<td>` tags,
|
||||
* but it doesn't make sense to allow for a "summary" attribute on these tags.
|
||||
* Hence these would need to be split in two separate rules.
|
||||
*
|
||||
* HTML rules must be added with the `addHTMLRule()` method. A feature that
|
||||
* has zero HTML rules does not create or modify HTML.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the feature.
|
||||
*
|
||||
* @see Drupal.EditorFeatureHTMLRule
|
||||
*/
|
||||
Drupal.EditorFeature = function (name) {
|
||||
this.name = name;
|
||||
this.rules = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a HTML rule to the list of HTML rules for this feature.
|
||||
*
|
||||
* @param {Drupal.EditorFeatureHTMLRule} rule
|
||||
* A text editor feature HTML rule.
|
||||
*/
|
||||
Drupal.EditorFeature.prototype.addHTMLRule = function (rule) {
|
||||
this.rules.push(rule);
|
||||
};
|
||||
|
||||
/**
|
||||
* Text filter status object. Initialized with the filter ID.
|
||||
*
|
||||
* Indicates whether the text filter is currently active (enabled) or not.
|
||||
*
|
||||
* Contains a set of HTML rules ({@link Drupal.FilterHTMLRule} objects) that
|
||||
* describe which HTML tags are allowed or forbidden. They can also describe
|
||||
* for a set of tags (or all tags) which attributes, styles and classes are
|
||||
* allowed and which are forbidden.
|
||||
*
|
||||
* It is necessary to allow for multiple HTML rules per feature, for
|
||||
* analogous reasons as {@link Drupal.EditorFeature}.
|
||||
*
|
||||
* HTML rules must be added with the `addHTMLRule()` method. A filter that has
|
||||
* zero HTML rules does not disallow any HTML.
|
||||
*
|
||||
* @constructor
|
||||
*
|
||||
* @param {string} name
|
||||
* The name of the feature.
|
||||
*
|
||||
* @see Drupal.FilterHTMLRule
|
||||
*/
|
||||
Drupal.FilterStatus = function (name) {
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
this.name = name;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
this.active = false;
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Array.<Drupal.FilterHTMLRule>}
|
||||
*/
|
||||
this.rules = [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Adds a HTML rule to the list of HTML rules for this filter.
|
||||
*
|
||||
* @param {Drupal.FilterHTMLRule} rule
|
||||
* A text filter HTML rule.
|
||||
*/
|
||||
Drupal.FilterStatus.prototype.addHTMLRule = function (rule) {
|
||||
this.rules.push(rule);
|
||||
};
|
||||
|
||||
/**
|
||||
* A text filter HTML rule object.
|
||||
*
|
||||
* Intended to be used in combination with {@link Drupal.FilterStatus}.
|
||||
*
|
||||
* A text filter rule object describes:
|
||||
* 1. allowed or forbidden tags: (optional) whitelist or blacklist HTML tags
|
||||
* 2. restricted tag properties: (optional) whitelist or blacklist
|
||||
* attributes, styles and classes on a set of HTML tags.
|
||||
*
|
||||
* Typically, each text filter rule object does either 1 or 2, not both.
|
||||
*
|
||||
* The structure can be very clearly seen below:
|
||||
* 1. use the "tags" key to list HTML tags, and set the "allow" key to
|
||||
* either true (to allow these HTML tags) or false (to forbid these HTML
|
||||
* tags). If you leave the "tags" key's default value (the empty array),
|
||||
* no restrictions are applied.
|
||||
* 2. all nested within the "restrictedTags" key: use the "tags" subkey to
|
||||
* list HTML tags to which you want to apply property restrictions, then
|
||||
* use the "allowed" subkey to whitelist specific property values, and
|
||||
* similarly use the "forbidden" subkey to blacklist specific property
|
||||
* values.
|
||||
*
|
||||
* @example
|
||||
* <caption>Whitelist the "p", "strong" and "a" HTML tags.</caption>
|
||||
* {
|
||||
* tags: ['p', 'strong', 'a'],
|
||||
* allow: true,
|
||||
* restrictedTags: {
|
||||
* tags: [],
|
||||
* allowed: { attributes: [], styles: [], classes: [] },
|
||||
* forbidden: { attributes: [], styles: [], classes: [] }
|
||||
* }
|
||||
* }
|
||||
* @example
|
||||
* <caption>For the "a" HTML tag, only allow the "href" attribute
|
||||
* and the "external" class and disallow the "target" attribute.</caption>
|
||||
* {
|
||||
* tags: [],
|
||||
* allow: null,
|
||||
* restrictedTags: {
|
||||
* tags: ['a'],
|
||||
* allowed: { attributes: ['href'], styles: [], classes: ['external'] },
|
||||
* forbidden: { attributes: ['target'], styles: [], classes: [] }
|
||||
* }
|
||||
* }
|
||||
* @example
|
||||
* <caption>For all tags, allow the "data-*" attribute (that is, any
|
||||
* attribute that begins with "data-").</caption>
|
||||
* {
|
||||
* tags: [],
|
||||
* allow: null,
|
||||
* restrictedTags: {
|
||||
* tags: ['*'],
|
||||
* allowed: { attributes: ['data-*'], styles: [], classes: [] },
|
||||
* forbidden: { attributes: [], styles: [], classes: [] }
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @return {{
|
||||
* tags: Array,
|
||||
* allow: null,
|
||||
* restrictedTags: {
|
||||
* tags: Array,
|
||||
* allowed: {attributes: Array, styles: Array, classes: Array},
|
||||
* forbidden: {attributes: Array, styles: Array, classes: Array}
|
||||
* }
|
||||
* }}
|
||||
*
|
||||
* @see Drupal.FilterStatus
|
||||
*/
|
||||
Drupal.FilterHTMLRule = function () {
|
||||
return {
|
||||
// Allow or forbid tags.
|
||||
tags: [],
|
||||
allow: null,
|
||||
// Apply restrictions to properties set on tags.
|
||||
restrictedTags: {
|
||||
tags: [],
|
||||
allowed: {attributes: [], styles: [], classes: []},
|
||||
forbidden: {attributes: [], styles: [], classes: []}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Tracks the configuration of all text filters in {@link Drupal.FilterStatus}
|
||||
* objects for {@link Drupal.editorConfiguration.featureIsAllowedByFilters}.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
Drupal.filterConfiguration = {
|
||||
|
||||
/**
|
||||
* Drupal.FilterStatus objects, keyed by filter ID.
|
||||
*
|
||||
* @type {Object.<string, Drupal.FilterStatus>}
|
||||
*/
|
||||
statuses: {},
|
||||
|
||||
/**
|
||||
* Live filter setting parsers.
|
||||
*
|
||||
* Object keyed by filter ID, for those filters that implement it.
|
||||
*
|
||||
* Filters should load the implementing JavaScript on the filter
|
||||
* configuration form and implement
|
||||
* `Drupal.filterSettings[filterID].getRules()`, which should return an
|
||||
* array of {@link Drupal.FilterHTMLRule} objects.
|
||||
*
|
||||
* @namespace
|
||||
*/
|
||||
liveSettingParsers: {},
|
||||
|
||||
/**
|
||||
* Updates all {@link Drupal.FilterStatus} objects to reflect current state.
|
||||
*
|
||||
* Automatically checks whether a filter is currently enabled or not. To
|
||||
* support more finegrained.
|
||||
*
|
||||
* If a filter implements a live setting parser, then that will be used to
|
||||
* keep the HTML rules for the {@link Drupal.FilterStatus} object
|
||||
* up-to-date.
|
||||
*/
|
||||
update: function () {
|
||||
for (var filterID in Drupal.filterConfiguration.statuses) {
|
||||
if (Drupal.filterConfiguration.statuses.hasOwnProperty(filterID)) {
|
||||
// Update status.
|
||||
Drupal.filterConfiguration.statuses[filterID].active = $('[name="filters[' + filterID + '][status]"]').is(':checked');
|
||||
|
||||
// Update current rules.
|
||||
if (Drupal.filterConfiguration.liveSettingParsers[filterID]) {
|
||||
Drupal.filterConfiguration.statuses[filterID].rules = Drupal.filterConfiguration.liveSettingParsers[filterID].getRules();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Initializes {@link Drupal.filterConfiguration}.
|
||||
*
|
||||
* @type {Drupal~behavior}
|
||||
*/
|
||||
Drupal.behaviors.initializeFilterConfiguration = {
|
||||
attach: function (context, settings) {
|
||||
var $context = $(context);
|
||||
|
||||
$context.find('#filters-status-wrapper input.form-checkbox').once('filter-editor-status').each(function () {
|
||||
var $checkbox = $(this);
|
||||
var nameAttribute = $checkbox.attr('name');
|
||||
|
||||
// The filter's checkbox has a name attribute of the form
|
||||
// "filters[<name of filter>][status]", parse "<name of filter>"
|
||||
// from it.
|
||||
var filterID = nameAttribute.substring(8, nameAttribute.indexOf(']'));
|
||||
|
||||
// Create a Drupal.FilterStatus object to track the state (whether it's
|
||||
// active or not and its current settings, if any) of each filter.
|
||||
Drupal.filterConfiguration.statuses[filterID] = new Drupal.FilterStatus(filterID);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
})(jQuery, _, Drupal, document);
|
30
core/modules/editor/js/editor.dialog.js
Normal file
30
core/modules/editor/js/editor.dialog.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
/**
|
||||
* @file
|
||||
* AJAX commands used by Editor module.
|
||||
*/
|
||||
|
||||
(function ($, Drupal) {
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Command to save the contents of an editor-provided modal.
|
||||
*
|
||||
* This command does not close the open modal. It should be followed by a
|
||||
* call to `Drupal.AjaxCommands.prototype.closeDialog`. Editors that are
|
||||
* integrated with dialogs must independently listen for an
|
||||
* `editor:dialogsave` event to save the changes into the contents of their
|
||||
* interface.
|
||||
*
|
||||
* @param {Drupal.Ajax} [ajax]
|
||||
* @param {object} response
|
||||
* @param {Array} response.values
|
||||
* @param {number} [status]
|
||||
*
|
||||
* @fires event:editor:dialogsave
|
||||
*/
|
||||
Drupal.AjaxCommands.prototype.editorDialogSave = function (ajax, response, status) {
|
||||
$(window).trigger('editor:dialogsave', [response.values]);
|
||||
};
|
||||
|
||||
})(jQuery, Drupal);
|
220
core/modules/editor/js/editor.formattedTextEditor.js
Normal file
220
core/modules/editor/js/editor.formattedTextEditor.js
Normal file
|
@ -0,0 +1,220 @@
|
|||
/**
|
||||
* @file
|
||||
* Text editor-based in-place editor for formatted text content in Drupal.
|
||||
*
|
||||
* Depends on editor.module. Works with any (WYSIWYG) editor that implements the
|
||||
* editor.js API, including the optional attachInlineEditor() and onChange()
|
||||
* methods.
|
||||
* For example, assuming that a hypothetical editor's name was "Magical Editor"
|
||||
* and its editor.js API implementation lived at Drupal.editors.magical, this
|
||||
* JavaScript would use:
|
||||
* - Drupal.editors.magical.attachInlineEditor()
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
|
||||
"use strict";
|
||||
|
||||
Drupal.quickedit.editors.editor = Drupal.quickedit.EditorView.extend(/** @lends Drupal.quickedit.editors.editor# */{
|
||||
|
||||
/**
|
||||
* The text format for this field.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
textFormat: null,
|
||||
|
||||
/**
|
||||
* Indicates whether this text format has transformations.
|
||||
*
|
||||
* @type {bool}
|
||||
*/
|
||||
textFormatHasTransformations: null,
|
||||
|
||||
/**
|
||||
* Stores a reference to the text editor object for this field.
|
||||
*
|
||||
* @type {Drupal.quickedit.EditorModel}
|
||||
*/
|
||||
textEditor: null,
|
||||
|
||||
/**
|
||||
* Stores the textual DOM element that is being in-place edited.
|
||||
*
|
||||
* @type {jQuery}
|
||||
*/
|
||||
$textElement: null,
|
||||
|
||||
/**
|
||||
* @constructs
|
||||
*
|
||||
* @augments Drupal.quickedit.EditorView
|
||||
*
|
||||
* @param {object} options
|
||||
*/
|
||||
initialize: function (options) {
|
||||
Drupal.quickedit.EditorView.prototype.initialize.call(this, options);
|
||||
|
||||
var 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.
|
||||
this.$textElement = this.$el.find('.field-item').eq(0);
|
||||
this.model.set('originalValue', this.$textElement.html());
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {jQuery}
|
||||
*/
|
||||
getEditedElement: function () {
|
||||
return this.$textElement;
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @param {object} fieldModel
|
||||
* @param {string} state
|
||||
*/
|
||||
stateChange: function (fieldModel, state) {
|
||||
var editorModel = this.model;
|
||||
var from = fieldModel.previous('state');
|
||||
var 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 been applied to the formatted
|
||||
// text of this field, then we'll need to load a re-formatted version
|
||||
// of it without the transformation filters.
|
||||
if (this.textFormatHasTransformations) {
|
||||
var $textElement = this.$textElement;
|
||||
this._getUntransformedText(function (untransformedText) {
|
||||
$textElement.html(untransformedText);
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
}
|
||||
// When no transformation filters have been applied: start WYSIWYG
|
||||
// editing immediately!
|
||||
else {
|
||||
// Defer updating the model until the current state change has
|
||||
// propagated, to not trigger a nested state change event.
|
||||
_.defer(function () {
|
||||
fieldModel.set('state', 'active');
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'active':
|
||||
var textElement = this.$textElement.get(0);
|
||||
var toolbarView = fieldModel.toolbarView;
|
||||
this.textEditor.attachInlineEditor(
|
||||
textElement,
|
||||
this.textFormat,
|
||||
toolbarView.getMainWysiwygToolgroupId(),
|
||||
toolbarView.getFloatedWysiwygToolgroupId()
|
||||
);
|
||||
// Set the state to 'changed' whenever the content has changed.
|
||||
this.textEditor.onChange(textElement, function (htmlText) {
|
||||
editorModel.set('currentValue', htmlText);
|
||||
fieldModel.set('state', 'changed');
|
||||
});
|
||||
break;
|
||||
|
||||
case 'changed':
|
||||
break;
|
||||
|
||||
case 'saving':
|
||||
if (from === 'invalid') {
|
||||
this.removeValidationErrors();
|
||||
}
|
||||
this.save();
|
||||
break;
|
||||
|
||||
case 'saved':
|
||||
break;
|
||||
|
||||
case 'invalid':
|
||||
this.showValidationErrors();
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*
|
||||
* @return {object}
|
||||
*/
|
||||
getQuickEditUISettings: function () {
|
||||
return {padding: true, unifiedToolbar: true, fullWidthToolbar: true, popup: false};
|
||||
},
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
revert: function () {
|
||||
this.$textElement.html(this.model.get('originalValue'));
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads untransformed text for this field.
|
||||
*
|
||||
* More accurately: it re-filters formatted text to exclude transformation
|
||||
* filters used by the text format.
|
||||
*
|
||||
* @param {function} callback
|
||||
* A callback function that will receive the untransformed text.
|
||||
*
|
||||
* @see \Drupal\editor\Ajax\GetUntransformedTextCommand
|
||||
*/
|
||||
_getUntransformedText: function (callback) {
|
||||
var fieldID = this.fieldModel.get('fieldID');
|
||||
|
||||
// Create a Drupal.ajax instance to load the form.
|
||||
var textLoaderAjax = Drupal.ajax({
|
||||
url: Drupal.quickedit.util.buildUrl(fieldID, Drupal.url('editor/!entity_type/!id/!field_name/!langcode/!view_mode')),
|
||||
submit: {nocssjs: true}
|
||||
});
|
||||
|
||||
// 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);
|
327
core/modules/editor/js/editor.js
Normal file
327
core/modules/editor/js/editor.js
Normal file
|
@ -0,0 +1,327 @@
|
|||
/**
|
||||
* @file
|
||||
* Attaches behavior for the Editor module.
|
||||
*/
|
||||
|
||||
(function ($, Drupal, drupalSettings) {
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Finds the text area field associated with the given text format selector.
|
||||
*
|
||||
* @param {jQuery} $formatSelector
|
||||
* A text format selector DOM element.
|
||||
*
|
||||
* @return {HTMLElement}
|
||||
* The text area DOM element, if it was found.
|
||||
*/
|
||||
function findFieldForFormatSelector($formatSelector) {
|
||||
var field_id = $formatSelector.attr('data-editor-for');
|
||||
// This selector will only find text areas in the top-level document. We do
|
||||
// not support attaching editors on text areas within iframes.
|
||||
return $('#' + field_id).get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the text editor on a text area.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The text area DOM element.
|
||||
* @param {string} newFormatID
|
||||
* The text format we're changing to; the text editor for the currently
|
||||
* active text format will be detached, and the text editor for the new text
|
||||
* format will be attached.
|
||||
*/
|
||||
function changeTextEditor(field, newFormatID) {
|
||||
var previousFormatID = field.getAttribute('data-editor-active-text-format');
|
||||
|
||||
// Detach the current editor (if any) and attach a new editor.
|
||||
if (drupalSettings.editor.formats[previousFormatID]) {
|
||||
Drupal.editorDetach(field, drupalSettings.editor.formats[previousFormatID]);
|
||||
}
|
||||
// When no text editor is currently active, stop tracking changes.
|
||||
else {
|
||||
$(field).off('.editor');
|
||||
}
|
||||
|
||||
// Attach the new text editor (if any).
|
||||
if (drupalSettings.editor.formats[newFormatID]) {
|
||||
var format = drupalSettings.editor.formats[newFormatID];
|
||||
filterXssWhenSwitching(field, format, previousFormatID, Drupal.editorAttach);
|
||||
}
|
||||
|
||||
// Store the new active format.
|
||||
field.setAttribute('data-editor-active-text-format', newFormatID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles changes in text format.
|
||||
*
|
||||
* @param {jQuery.Event} event
|
||||
*/
|
||||
function onTextFormatChange(event) {
|
||||
var $select = $(event.target);
|
||||
var field = event.data.field;
|
||||
var activeFormatID = field.getAttribute('data-editor-active-text-format');
|
||||
var newFormatID = $select.val();
|
||||
|
||||
// Prevent double-attaching if the change event is triggered manually.
|
||||
if (newFormatID === activeFormatID) {
|
||||
return;
|
||||
}
|
||||
|
||||
// When changing to a text format that has a text editor associated
|
||||
// with it that supports content filtering, then first ask for
|
||||
// confirmation, because switching text formats might cause certain
|
||||
// markup to be stripped away.
|
||||
var supportContentFiltering = drupalSettings.editor.formats[newFormatID] && drupalSettings.editor.formats[newFormatID].editorSupportsContentFiltering;
|
||||
// If there is no content yet, it's always safe to change the text format.
|
||||
var hasContent = field.value !== '';
|
||||
if (hasContent && supportContentFiltering) {
|
||||
var message = Drupal.t('Changing the text format to %text_format will permanently remove content that is not allowed in that text format.<br><br>Save your changes before switching the text format to avoid losing data.', {
|
||||
'%text_format': $select.find('option:selected').text()
|
||||
});
|
||||
var confirmationDialog = Drupal.dialog('<div>' + message + '</div>', {
|
||||
title: Drupal.t('Change text format?'),
|
||||
dialogClass: 'editor-change-text-format-modal',
|
||||
resizable: false,
|
||||
buttons: [
|
||||
{
|
||||
text: Drupal.t('Continue'),
|
||||
'class': 'button button--primary',
|
||||
click: function () {
|
||||
changeTextEditor(field, newFormatID);
|
||||
confirmationDialog.close();
|
||||
}
|
||||
},
|
||||
{
|
||||
text: Drupal.t('Cancel'),
|
||||
'class': 'button',
|
||||
click: function () {
|
||||
// Restore the active format ID: cancel changing text format. We cannot
|
||||
// simply call event.preventDefault() because jQuery's change event is
|
||||
// only triggered after the change has already been accepted.
|
||||
$select.val(activeFormatID);
|
||||
confirmationDialog.close();
|
||||
}
|
||||
}
|
||||
],
|
||||
// Prevent this modal from being closed without the user making a choice
|
||||
// as per http://stackoverflow.com/a/5438771.
|
||||
closeOnEscape: false,
|
||||
create: function () {
|
||||
$(this).parent().find('.ui-dialog-titlebar-close').remove();
|
||||
},
|
||||
beforeClose: false,
|
||||
close: function (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}
|
||||
*/
|
||||
Drupal.behaviors.editor = {
|
||||
attach: function (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 () {
|
||||
var $this = $(this);
|
||||
var field = findFieldForFormatSelector($this);
|
||||
|
||||
// Opt-out if no supported text area was found.
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the current active format.
|
||||
var activeFormatID = $this.val();
|
||||
field.setAttribute('data-editor-active-text-format', activeFormatID);
|
||||
|
||||
// Directly attach this text editor, if the text format is enabled.
|
||||
if (settings.editor.formats[activeFormatID]) {
|
||||
// XSS protection for the current text format/editor is performed on the
|
||||
// server side, so we don't need to do anything special here.
|
||||
Drupal.editorAttach(field, settings.editor.formats[activeFormatID]);
|
||||
}
|
||||
// When there is no text editor for this text format, still track changes,
|
||||
// because the user has the ability to switch to some text editor, other-
|
||||
// wise this code would not be executed.
|
||||
$(field).on('change.editor keypress.editor', function () {
|
||||
field.setAttribute('data-editor-value-is-changed', 'true');
|
||||
// Just knowing that the value was changed is enough, stop tracking.
|
||||
$(field).off('.editor');
|
||||
});
|
||||
|
||||
// Attach onChange handler to text format selector element.
|
||||
if ($this.is('select')) {
|
||||
$this.on('change.editorAttach', {field: field}, onTextFormatChange);
|
||||
}
|
||||
// Detach any editor when the containing form is submitted.
|
||||
$this.parents('form').on('submit', function (event) {
|
||||
// Do not detach if the event was canceled.
|
||||
if (event.isDefaultPrevented()) {
|
||||
return;
|
||||
}
|
||||
// Detach the current editor (if any).
|
||||
if (settings.editor.formats[activeFormatID]) {
|
||||
Drupal.editorDetach(field, settings.editor.formats[activeFormatID], 'serialize');
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
detach: function (context, settings, trigger) {
|
||||
var editors;
|
||||
// The 'serialize' trigger indicates that we should simply update the
|
||||
// underlying element with the new text, without destroying the editor.
|
||||
if (trigger === 'serialize') {
|
||||
// Removing the editor-processed class guarantees that the editor will
|
||||
// be reattached. Only do this if we're planning to destroy the editor.
|
||||
editors = $(context).find('[data-editor-for]').findOnce('editor');
|
||||
}
|
||||
else {
|
||||
editors = $(context).find('[data-editor-for]').removeOnce('editor');
|
||||
}
|
||||
|
||||
editors.each(function () {
|
||||
var $this = $(this);
|
||||
var activeFormatID = $this.val();
|
||||
var 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) {
|
||||
// HTML5 validation cannot ever work for WYSIWYG editors, because WYSIWYG
|
||||
// editors always hide the underlying textarea element, which prevents
|
||||
// browsers from putting the error message bubble in the right location.
|
||||
// Hence: disable HTML5 validation for this element.
|
||||
if ('required' in field.attributes) {
|
||||
field.setAttribute('data-editor-required', true);
|
||||
field.removeAttribute('required');
|
||||
}
|
||||
|
||||
// Attach the text editor.
|
||||
Drupal.editors[format.editor].attach(field, format);
|
||||
|
||||
// Ensures form.js' 'formUpdated' event is triggered even for changes that
|
||||
// happen within the text editor.
|
||||
Drupal.editors[format.editor].onChange(field, function () {
|
||||
$(field).trigger('formUpdated');
|
||||
|
||||
// Keep track of changes, so we know what to do when switching text
|
||||
// formats and guaranteeing XSS protection.
|
||||
field.setAttribute('data-editor-value-is-changed', 'true');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detaches editor behaviors from the field.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The textarea DOM element.
|
||||
* @param {object} format
|
||||
* The text format that's being activated, from
|
||||
* drupalSettings.editor.formats.
|
||||
* @param {string} trigger
|
||||
* Trigger value from the detach behavior.
|
||||
*/
|
||||
Drupal.editorDetach = function (field, format, trigger) {
|
||||
if (format.editor) {
|
||||
// Restore the HTML5 validation "required" attribute if it was removed in
|
||||
// Drupal.editorAttach().
|
||||
if ('data-editor-required' in field.attributes) {
|
||||
field.setAttribute('required', 'required');
|
||||
field.removeAttribute('data-editor-required');
|
||||
}
|
||||
|
||||
Drupal.editors[format.editor].detach(field, format, trigger);
|
||||
|
||||
// Restore the original value if the user didn't make any changes yet.
|
||||
if (field.getAttribute('data-editor-value-is-changed') === 'false') {
|
||||
field.value = field.getAttribute('data-editor-value-original');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Filter away XSS attack vectors when switching text formats.
|
||||
*
|
||||
* @param {HTMLElement} field
|
||||
* The textarea DOM element.
|
||||
* @param {object} format
|
||||
* The text format that's being activated, from
|
||||
* drupalSettings.editor.formats.
|
||||
* @param {string} originalFormatID
|
||||
* The text format ID of the original text format.
|
||||
* @param {function} callback
|
||||
* A callback to be called (with no parameters) after the field's value has
|
||||
* been XSS filtered.
|
||||
*/
|
||||
function filterXssWhenSwitching(field, format, originalFormatID, callback) {
|
||||
// A text editor that already is XSS-safe needs no additional measures.
|
||||
if (format.editor.isXssSafe) {
|
||||
callback(field, format);
|
||||
}
|
||||
// Otherwise, ensure XSS safety: let the server XSS filter this value.
|
||||
else {
|
||||
$.ajax({
|
||||
url: Drupal.url('editor/filter_xss/' + format.format),
|
||||
type: 'POST',
|
||||
data: {
|
||||
'value': field.value,
|
||||
'original_format_id': originalFormatID
|
||||
},
|
||||
dataType: 'json',
|
||||
success: function (xssFilteredValue) {
|
||||
// If the server returns false, then no XSS filtering is needed.
|
||||
if (xssFilteredValue !== false) {
|
||||
field.value = xssFilteredValue;
|
||||
}
|
||||
callback(field, format);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
})(jQuery, Drupal, drupalSettings);
|
47
core/modules/editor/src/Ajax/EditorDialogSave.php
Normal file
47
core/modules/editor/src/Ajax/EditorDialogSave.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Ajax\EditorDialogSave.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Ajax;
|
||||
|
||||
use Drupal\Core\Ajax\CommandInterface;
|
||||
|
||||
/**
|
||||
* Provides an AJAX command for saving the contents of an editor dialog.
|
||||
*
|
||||
* This command is implemented in editor.dialog.js in
|
||||
* Drupal.AjaxCommands.prototype.editorDialogSave.
|
||||
*/
|
||||
class EditorDialogSave implements CommandInterface {
|
||||
|
||||
/**
|
||||
* An array of values that will be passed back to the editor by the dialog.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $values;
|
||||
|
||||
/**
|
||||
* Constructs a EditorDialogSave object.
|
||||
*
|
||||
* @param string $values
|
||||
* The values that should be passed to the form constructor in Drupal.
|
||||
*/
|
||||
public function __construct($values) {
|
||||
$this->values = $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render() {
|
||||
return array(
|
||||
'command' => 'editorDialogSave',
|
||||
'values' => $this->values,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
29
core/modules/editor/src/Ajax/GetUntransformedTextCommand.php
Normal file
29
core/modules/editor/src/Ajax/GetUntransformedTextCommand.php
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Ajax\GetUntransformedTextCommand.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Ajax;
|
||||
|
||||
use Drupal\Core\Ajax\CommandInterface;
|
||||
use Drupal\quickedit\Ajax\BaseCommand;
|
||||
|
||||
/**
|
||||
* AJAX command to rerender a formatted text field without any transformation
|
||||
* filters.
|
||||
*/
|
||||
class GetUntransformedTextCommand extends BaseCommand {
|
||||
|
||||
/**
|
||||
* Constructs a GetUntransformedTextCommand object.
|
||||
*
|
||||
* @param string $data
|
||||
* The data to pass on to the client side.
|
||||
*/
|
||||
public function __construct($data) {
|
||||
parent::__construct('editorGetUntransformedText', $data);
|
||||
}
|
||||
|
||||
}
|
72
core/modules/editor/src/Annotation/Editor.php
Normal file
72
core/modules/editor/src/Annotation/Editor.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Annotation\Editor.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Annotation;
|
||||
|
||||
use Drupal\Component\Annotation\Plugin;
|
||||
|
||||
/**
|
||||
* Defines an Editor annotation object.
|
||||
*
|
||||
* Plugin Namespace: Plugin\Editor
|
||||
*
|
||||
* For a working example, see \Drupal\ckeditor\Plugin\Editor\CKEditor
|
||||
*
|
||||
* @see \Drupal\editor\Plugin\EditorPluginInterface
|
||||
* @see \Drupal\editor\Plugin\EditorBase
|
||||
* @see \Drupal\editor\Plugin\EditorManager
|
||||
* @see plugin_api
|
||||
*
|
||||
* @Annotation
|
||||
*/
|
||||
class Editor extends Plugin {
|
||||
|
||||
/**
|
||||
* The plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* The human-readable name of the editor plugin.
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\Translation
|
||||
*/
|
||||
public $label;
|
||||
|
||||
/**
|
||||
* Whether the editor supports "allowed content only" filtering.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $supports_content_filtering;
|
||||
|
||||
/**
|
||||
* Whether the editor supports the inline editing provided by the Edit module.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $supports_inline_editing;
|
||||
|
||||
/**
|
||||
* Whether this text editor is not vulnerable to XSS attacks.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $is_xss_safe;
|
||||
|
||||
/**
|
||||
* A list of element types this text editor supports.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $supported_element_types;
|
||||
|
||||
}
|
87
core/modules/editor/src/EditorController.php
Normal file
87
core/modules/editor/src/EditorController.php
Normal file
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\EditorController.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor;
|
||||
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\OpenModalDialogCommand;
|
||||
use Drupal\Core\Ajax\CloseModalDialogCommand;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\editor\Ajax\GetUntransformedTextCommand;
|
||||
use Drupal\editor\Form\EditorImageDialog;
|
||||
use Drupal\editor\Form\EditorLinkDialog;
|
||||
use Drupal\filter\Plugin\FilterInterface;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Returns responses for Editor module routes.
|
||||
*/
|
||||
class EditorController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* Returns an Ajax response to render a text field without transformation filters.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity of which a formatted text field is being rerendered.
|
||||
* @param string $field_name
|
||||
* The name of the (formatted text) field that that is being rerendered
|
||||
* @param string $langcode
|
||||
* The name of the language for which the formatted text field is being
|
||||
* rerendered.
|
||||
* @param string $view_mode_id
|
||||
* The view mode the formatted text field should be rerendered in.
|
||||
*
|
||||
* @return \Drupal\Core\Ajax\AjaxResponse
|
||||
* The Ajax response.
|
||||
*/
|
||||
public function getUntransformedText(EntityInterface $entity, $field_name, $langcode, $view_mode_id) {
|
||||
$response = new AjaxResponse();
|
||||
|
||||
// Direct text editing is only supported for single-valued fields.
|
||||
$field = $entity->getTranslation($langcode)->$field_name;
|
||||
$editable_text = check_markup($field->value, $field->format, $langcode, array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE));
|
||||
$response->addCommand(new GetUntransformedTextCommand($editable_text));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the necessary XSS filtering for using a certain text format's editor.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The current request object.
|
||||
* @param \Drupal\filter\FilterFormatInterface $filter_format
|
||||
* The text format whose text editor (if any) will be used.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse
|
||||
* A JSON response containing the XSS-filtered value.
|
||||
*
|
||||
* @see editor_filter_xss()
|
||||
*/
|
||||
public function filterXss(Request $request, FilterFormatInterface $filter_format) {
|
||||
$value = $request->request->get('value');
|
||||
if (!isset($value)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
// The original_format parameter will only exist when switching text format.
|
||||
$original_format_id = $request->request->get('original_format_id');
|
||||
$original_format = NULL;
|
||||
if (isset($original_format_id)) {
|
||||
$original_format = $this->entityManager()
|
||||
->getStorage('filter_format')
|
||||
->load($original_format_id);
|
||||
}
|
||||
|
||||
return new JsonResponse(editor_filter_xss($value, $filter_format, $original_format));
|
||||
}
|
||||
|
||||
}
|
90
core/modules/editor/src/EditorInterface.php
Normal file
90
core/modules/editor/src/EditorInterface.php
Normal file
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\EditorInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface defining a text editor entity.
|
||||
*/
|
||||
interface EditorInterface extends ConfigEntityInterface {
|
||||
|
||||
/**
|
||||
* Returns whether this text editor has an associated filter format.
|
||||
*
|
||||
* A text editor may be created at the same time as the filter format it's
|
||||
* going to be associated with; in that case, no filter format object is
|
||||
* available yet.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAssociatedFilterFormat();
|
||||
|
||||
/**
|
||||
* Returns the filter format this text editor is associated with.
|
||||
*
|
||||
* This could be NULL if the associated filter format is still being created.
|
||||
* @see hasAssociatedFilterFormat()
|
||||
*
|
||||
* @return \Drupal\filter\FilterFormatInterface|null
|
||||
*/
|
||||
public function getFilterFormat();
|
||||
|
||||
/**
|
||||
* Returns the associated text editor plugin ID.
|
||||
*
|
||||
* @return string
|
||||
* The text editor plugin ID.
|
||||
*/
|
||||
public function getEditor();
|
||||
|
||||
/**
|
||||
* Set the text editor plugin ID.
|
||||
*
|
||||
* @param string $editor
|
||||
* The text editor plugin ID to set.
|
||||
*/
|
||||
public function setEditor($editor);
|
||||
|
||||
/**
|
||||
* Returns the text editor plugin-specific settings.
|
||||
*
|
||||
* @return array
|
||||
* A structured array containing all text editor settings.
|
||||
*/
|
||||
public function getSettings();
|
||||
|
||||
/**
|
||||
* Sets the text editor plugin-specific settings.
|
||||
*
|
||||
* @param array $settings
|
||||
* The structured array containing all text editor settings.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSettings(array $settings);
|
||||
|
||||
/**
|
||||
* Returns the image upload settings.
|
||||
*
|
||||
* @return array
|
||||
* A structured array containing image upload settings.
|
||||
*/
|
||||
public function getImageUploadSettings();
|
||||
|
||||
/**
|
||||
* Sets the image upload settings.
|
||||
*
|
||||
* @param array $image_upload
|
||||
* The structured array containing image upload settings.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setImageUploadSettings(array $image_upload);
|
||||
|
||||
}
|
179
core/modules/editor/src/EditorXssFilter/Standard.php
Normal file
179
core/modules/editor/src/EditorXssFilter/Standard.php
Normal file
|
@ -0,0 +1,179 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\EditorXssFilter\Standard.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\EditorXssFilter;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\Xss;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Drupal\editor\EditorXssFilterInterface;
|
||||
|
||||
/**
|
||||
* Defines the standard text editor XSS filter.
|
||||
*/
|
||||
class Standard extends Xss implements EditorXssFilterInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function filterXss($html, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL) {
|
||||
// Apply XSS filtering, but blacklist the <script>, <style>, <link>, <embed>
|
||||
// and <object> tags.
|
||||
// The <script> and <style> tags are blacklisted because their contents
|
||||
// can be malicious (and therefor they are inherently unsafe), whereas for
|
||||
// all other tags, only their attributes can make them malicious. Since
|
||||
// \Drupal\Component\Utility\Xss::filter() protects against malicious
|
||||
// attributes, we take no blacklisting action.
|
||||
// The exceptions to the above rule are <link>, <embed> and <object>:
|
||||
// - <link> because the href attribute allows the attacker to import CSS
|
||||
// using the HTTP(S) protocols which Xss::filter() considers safe by
|
||||
// default. The imported remote CSS is applied to the main document, thus
|
||||
// allowing for the same XSS attacks as a regular <style> tag.
|
||||
// - <embed> and <object> because these tags allow non-HTML applications or
|
||||
// content to be embedded using the src or data attributes, respectively.
|
||||
// This is safe in the case of HTML documents, but not in the case of
|
||||
// Flash objects for example, that may access/modify the main document
|
||||
// directly.
|
||||
// <iframe> is considered safe because it only allows HTML content to be
|
||||
// embedded, hence ensuring the same origin policy always applies.
|
||||
$dangerous_tags = array('script', 'style', 'link', 'embed', 'object');
|
||||
|
||||
// Simply blacklisting these five dangerous tags would bring safety, but
|
||||
// also user frustration: what if a text format is configured to allow
|
||||
// <embed>, for example? Then we would strip that tag, even though it is
|
||||
// allowed, thereby causing data loss!
|
||||
// Therefor, we want to be smarter still. We want to take into account which
|
||||
// HTML tags are allowed and forbidden by the text format we're filtering
|
||||
// for, and if we're switching from another text format, we want to take
|
||||
// that format's allowed and forbidden tags into account as well.
|
||||
// In other words: we only expect markup allowed in both the original and
|
||||
// the new format to continue to exist.
|
||||
$format_restrictions = $format->getHtmlRestrictions();
|
||||
if ($original_format !== NULL) {
|
||||
$original_format_restrictions = $original_format->getHtmlRestrictions();
|
||||
}
|
||||
|
||||
// Any tags that are explicitly blacklisted by the text format must be
|
||||
// appended to the list of default dangerous tags: if they're explicitly
|
||||
// forbidden, then we must respect that configuration.
|
||||
// When switching from another text format, we must use the union of
|
||||
// forbidden tags: if either text format is more restrictive, then the
|
||||
// safety expectations of *both* text formats apply.
|
||||
$forbidden_tags = self::getForbiddenTags($format_restrictions);
|
||||
if ($original_format !== NULL) {
|
||||
$forbidden_tags = array_merge($forbidden_tags, self::getForbiddenTags($original_format_restrictions));
|
||||
}
|
||||
|
||||
// Any tags that are explicitly whitelisted by the text format must be
|
||||
// removed from the list of default dangerous tags: if they're explicitly
|
||||
// allowed, then we must respect that configuration.
|
||||
// When switching from another format, we must use the intersection of
|
||||
// allowed tags: if either format is more restrictive, then the safety
|
||||
// expectations of *both* formats apply.
|
||||
$allowed_tags = self::getAllowedTags($format_restrictions);
|
||||
if ($original_format !== NULL) {
|
||||
$allowed_tags = array_intersect($allowed_tags, self::getAllowedTags($original_format_restrictions));
|
||||
}
|
||||
|
||||
// Don't blacklist dangerous tags that are explicitly allowed in both text
|
||||
// formats.
|
||||
$blacklisted_tags = array_diff($dangerous_tags, $allowed_tags);
|
||||
|
||||
// Also blacklist tags that are explicitly forbidden in either text format.
|
||||
$blacklisted_tags = array_merge($blacklisted_tags, $forbidden_tags);
|
||||
|
||||
$output = static::filter($html, $blacklisted_tags);
|
||||
|
||||
// Since data-attributes can contain encoded HTML markup that could be
|
||||
// decoded and interpreted by editors, we need to apply XSS filtering to
|
||||
// their contents.
|
||||
return static::filterXssDataAttributes($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a very permissive XSS/HTML filter to data-attributes.
|
||||
*
|
||||
* @param string $html
|
||||
* The string to apply the data-attributes filtering to.
|
||||
*
|
||||
* @return string
|
||||
* The filtered string.
|
||||
*/
|
||||
protected static function filterXssDataAttributes($html) {
|
||||
if (stristr($html, 'data-') !== FALSE) {
|
||||
$dom = Html::load($html);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
foreach ($xpath->query('//@*[starts-with(name(.), "data-")]') as $node) {
|
||||
// The data-attributes contain an HTML-encoded value, so we need to
|
||||
// decode the value, apply XSS filtering and then re-save as encoded
|
||||
// value. There is no need to explicitly decode $node->value, since the
|
||||
// DOMAttr::value getter returns the decoded value.
|
||||
$value = Xss::filterAdmin($node->value);
|
||||
$node->value = SafeMarkup::checkPlain($value);
|
||||
}
|
||||
$html = Html::serialize($dom);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all allowed tags from a restrictions data structure.
|
||||
*
|
||||
* @param array|FALSE $restrictions
|
||||
* Restrictions as returned by FilterInterface::getHTMLRestrictions().
|
||||
*
|
||||
* @return array
|
||||
* An array of allowed HTML tags.
|
||||
*
|
||||
* @see \Drupal\filter\Plugin\Filter\FilterInterface::getHTMLRestrictions()
|
||||
*/
|
||||
protected static function getAllowedTags($restrictions) {
|
||||
if ($restrictions === FALSE || !isset($restrictions['allowed'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$allowed_tags = array_keys($restrictions['allowed']);
|
||||
// Exclude the wildcard tag, which is used to set attribute restrictions on
|
||||
// all tags simultaneously.
|
||||
$allowed_tags = array_diff($allowed_tags, array('*'));
|
||||
|
||||
return $allowed_tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all forbidden tags from a restrictions data structure.
|
||||
*
|
||||
* @param array|FALSE $restrictions
|
||||
* Restrictions as returned by FilterInterface::getHTMLRestrictions().
|
||||
*
|
||||
* @return array
|
||||
* An array of forbidden HTML tags.
|
||||
*
|
||||
* @see \Drupal\filter\Plugin\Filter\FilterInterface::getHTMLRestrictions()
|
||||
*/
|
||||
protected static function getForbiddenTags($restrictions) {
|
||||
if ($restrictions === FALSE || !isset($restrictions['forbidden_tags'])) {
|
||||
return array();
|
||||
}
|
||||
else {
|
||||
return $restrictions['forbidden_tags'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function needsRemoval($html_tags, $elem) {
|
||||
// See static::filterXss() about how this class uses blacklisting instead
|
||||
// of the normal whitelisting.
|
||||
return !parent::needsRemoval($html_tags, $elem);
|
||||
}
|
||||
|
||||
}
|
47
core/modules/editor/src/EditorXssFilterInterface.php
Normal file
47
core/modules/editor/src/EditorXssFilterInterface.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\EditorXssFilterInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor;
|
||||
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
|
||||
/**
|
||||
* Defines an interface for text editor XSS (Cross-site scripting) filters.
|
||||
*/
|
||||
interface EditorXssFilterInterface {
|
||||
|
||||
/**
|
||||
* Filters HTML to prevent XSS attacks when a user edits it in a text editor.
|
||||
*
|
||||
* Should filter as minimally as possible, only to remove XSS attack vectors.
|
||||
*
|
||||
* Is only called when:
|
||||
* - loading a non-XSS-safe text editor for a $format that contains a filter
|
||||
* preventing XSS attacks (a FilterInterface::TYPE_HTML_RESTRICTOR filter):
|
||||
* if the output is safe, it should also be safe to edit.
|
||||
* - loading a non-XSS-safe text editor for a $format that doesn't contain a
|
||||
* filter preventing XSS attacks, but we're switching from a previous text
|
||||
* format ($original_format is not NULL) that did prevent XSS attacks: if
|
||||
* the output was previously safe, it should be safe to switch to another
|
||||
* text format and edit.
|
||||
*
|
||||
* @param string $html
|
||||
* The HTML to be filtered.
|
||||
* @param \Drupal\filter\FilterFormatInterface $format
|
||||
* The text format configuration entity. Provides context based upon which
|
||||
* one may want to adjust the filtering.
|
||||
* @param \Drupal\filter\FilterFormatInterface|null $original_format
|
||||
* (optional) The original text format configuration entity (when switching
|
||||
* text formats/editors). Also provides context based upon which one may
|
||||
* want to adjust the filtering.
|
||||
*
|
||||
* @return string
|
||||
* The filtered HTML that cannot cause any XSSes anymore.
|
||||
*/
|
||||
public static function filterXss($html, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL);
|
||||
|
||||
}
|
122
core/modules/editor/src/Element.php
Normal file
122
core/modules/editor/src/Element.php
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Element.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
|
||||
/**
|
||||
* Defines a service for Text Editor's render elements.
|
||||
*/
|
||||
class Element {
|
||||
|
||||
/**
|
||||
* The Text Editor plugin manager manager service.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected $pluginManager;
|
||||
|
||||
/**
|
||||
* Constructs a new Element object.
|
||||
*
|
||||
* @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
|
||||
* The Text Editor plugin manager service.
|
||||
*/
|
||||
public function __construct(PluginManagerInterface $plugin_manager) {
|
||||
$this->pluginManager = $plugin_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional #pre_render callback for 'text_format' elements.
|
||||
*/
|
||||
function preRenderTextFormat(array $element) {
|
||||
// Allow modules to programmatically enforce no client-side editor by
|
||||
// setting the #editor property to FALSE.
|
||||
if (isset($element['#editor']) && !$element['#editor']) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
// filter_process_format() copies properties to the expanded 'value' child
|
||||
// element, including the #pre_render property. Skip this text format
|
||||
// widget, if it contains no 'format'.
|
||||
if (!isset($element['format'])) {
|
||||
return $element;
|
||||
}
|
||||
$format_ids = array_keys($element['format']['format']['#options']);
|
||||
|
||||
// Early-return if no text editor is associated with any of the text formats.
|
||||
$editors = Editor::loadMultiple($format_ids);
|
||||
foreach ($editors as $key => $editor) {
|
||||
$definition = $this->pluginManager->getDefinition($editor->getEditor());
|
||||
if (!in_array($element['#base_type'], $definition['supported_element_types'])) {
|
||||
unset($editors[$key]);
|
||||
}
|
||||
}
|
||||
if (count($editors) === 0) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
// Use a hidden element for a single text format.
|
||||
$field_id = $element['value']['#id'];
|
||||
if (!$element['format']['format']['#access']) {
|
||||
// Use the first (and only) available text format.
|
||||
$format_id = $format_ids[0];
|
||||
$element['format']['editor'] = array(
|
||||
'#type' => 'hidden',
|
||||
'#name' => $element['format']['format']['#name'],
|
||||
'#value' => $format_id,
|
||||
'#attributes' => array(
|
||||
'data-editor-for' => $field_id,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Otherwise, attach to text format selector.
|
||||
else {
|
||||
$element['format']['format']['#attributes']['class'][] = 'editor';
|
||||
$element['format']['format']['#attributes']['data-editor-for'] = $field_id;
|
||||
}
|
||||
|
||||
// Hide the text format's filters' guidelines of those text formats that have
|
||||
// a text editor associated: they're rather useless when using a text editor.
|
||||
foreach ($editors as $format_id => $editor) {
|
||||
$element['format']['guidelines'][$format_id]['#access'] = FALSE;
|
||||
}
|
||||
|
||||
// Attach Text Editor module's (this module) library.
|
||||
$element['#attached']['library'][] = 'editor/drupal.editor';
|
||||
|
||||
// Attach attachments for all available editors.
|
||||
$element['#attached'] = drupal_merge_attached($element['#attached'], $this->pluginManager->getAttachments($format_ids));
|
||||
|
||||
// Apply XSS filters when editing content if necessary. Some types of text
|
||||
// editors cannot guarantee that the end user won't become a victim of XSS.
|
||||
if (!empty($element['value']['#value'])) {
|
||||
$original = $element['value']['#value'];
|
||||
$format = FilterFormat::load($element['format']['format']['#value']);
|
||||
|
||||
// Ensure XSS-safety for the current text format/editor.
|
||||
$filtered = editor_filter_xss($original, $format);
|
||||
if ($filtered !== FALSE) {
|
||||
$element['value']['#value'] = $filtered;
|
||||
}
|
||||
|
||||
// Only when the user has access to multiple text formats, we must add data-
|
||||
// attributes for the original value and change tracking, because they are
|
||||
// only necessary when the end user can switch between text formats/editors.
|
||||
if ($element['format']['format']['#access']) {
|
||||
$element['value']['#attributes']['data-editor-value-is-changed'] = 'false';
|
||||
$element['value']['#attributes']['data-editor-value-original'] = $original;
|
||||
}
|
||||
}
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
}
|
188
core/modules/editor/src/Entity/Editor.php
Normal file
188
core/modules/editor/src/Entity/Editor.php
Normal file
|
@ -0,0 +1,188 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Entity\Editor.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Entity;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityBase;
|
||||
use Drupal\editor\EditorInterface;
|
||||
|
||||
/**
|
||||
* Defines the configured text editor entity.
|
||||
*
|
||||
* @ConfigEntityType(
|
||||
* id = "editor",
|
||||
* label = @Translation("Text Editor"),
|
||||
* entity_keys = {
|
||||
* "id" = "format"
|
||||
* },
|
||||
* config_export = {
|
||||
* "format",
|
||||
* "editor",
|
||||
* "settings",
|
||||
* "image_upload",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class Editor extends ConfigEntityBase implements EditorInterface {
|
||||
|
||||
/**
|
||||
* The machine name of the text format with which this configured text editor
|
||||
* is associated.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @see getFilterFormat()
|
||||
*/
|
||||
protected $format;
|
||||
|
||||
/**
|
||||
* The name (plugin ID) of the text editor.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $editor;
|
||||
|
||||
/**
|
||||
* The structured array of text editor plugin-specific settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $settings = array();
|
||||
|
||||
/**
|
||||
* The structured array of image upload settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $image_upload = array();
|
||||
|
||||
/**
|
||||
* The filter format this text editor is associated with.
|
||||
*
|
||||
* @var \Drupal\filter\FilterFormatInterface
|
||||
*/
|
||||
protected $filterFormat;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected $editorPluginManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function id() {
|
||||
return $this->format;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(array $values, $entity_type) {
|
||||
parent::__construct($values, $entity_type);
|
||||
|
||||
$plugin = $this->editorPluginManager()->createInstance($this->editor);
|
||||
$this->settings += $plugin->getDefaultSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function label() {
|
||||
return $this->getFilterFormat()->label();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function calculateDependencies() {
|
||||
parent::calculateDependencies();
|
||||
// Create a dependency on the associated FilterFormat.
|
||||
$this->addDependency('config', $this->getFilterFormat()->getConfigDependencyName());
|
||||
// @todo use EntityWithPluginCollectionInterface so configuration between
|
||||
// config entity and dependency on provider is managed automatically.
|
||||
$definition = $this->editorPluginManager()->createInstance($this->editor)->getPluginDefinition();
|
||||
$this->addDependency('module', $definition['provider']);
|
||||
return $this->dependencies;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasAssociatedFilterFormat() {
|
||||
return $this->format !== NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilterFormat() {
|
||||
if (!$this->filterFormat) {
|
||||
$this->filterFormat = \Drupal::entityManager()->getStorage('filter_format')->load($this->format);
|
||||
}
|
||||
return $this->filterFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the editor plugin manager.
|
||||
*
|
||||
* @return \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected function editorPluginManager() {
|
||||
if (!$this->editorPluginManager) {
|
||||
$this->editorPluginManager = \Drupal::service('plugin.manager.editor');
|
||||
}
|
||||
|
||||
return $this->editorPluginManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getEditor() {
|
||||
return $this->editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setEditor($editor) {
|
||||
$this->editor = $editor;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSettings() {
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSettings(array $settings) {
|
||||
$this->settings = $settings;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getImageUploadSettings() {
|
||||
return $this->image_upload;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setImageUploadSettings(array $image_upload_settings) {
|
||||
$this->image_upload = $image_upload_settings;
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
243
core/modules/editor/src/Form/EditorImageDialog.php
Normal file
243
core/modules/editor/src/Form/EditorImageDialog.php
Normal file
|
@ -0,0 +1,243 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Form\EditorImageDialog.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Form;
|
||||
|
||||
use Drupal\Component\Utility\Bytes;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\HtmlCommand;
|
||||
use Drupal\editor\Ajax\EditorDialogSave;
|
||||
use Drupal\Core\Ajax\CloseModalDialogCommand;
|
||||
|
||||
/**
|
||||
* Provides an image dialog for text editors.
|
||||
*/
|
||||
class EditorImageDialog extends FormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'editor_image_dialog';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param \Drupal\filter\Entity\FilterFormat $filter_format
|
||||
* The filter format for which this dialog corresponds.
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, FilterFormat $filter_format = NULL) {
|
||||
// The default values are set directly from \Drupal::request()->request,
|
||||
// provided by the editor plugin opening the dialog.
|
||||
if (!$image_element = $form_state->get('image_element')) {
|
||||
$user_input = $form_state->getUserInput();
|
||||
$image_element = isset($user_input['editor_object']) ? $user_input['editor_object'] : [];
|
||||
$form_state->set('image_element', $image_element);
|
||||
}
|
||||
|
||||
$form['#tree'] = TRUE;
|
||||
$form['#attached']['library'][] = 'editor/drupal.editor.dialog';
|
||||
$form['#prefix'] = '<div id="editor-image-dialog-form">';
|
||||
$form['#suffix'] = '</div>';
|
||||
|
||||
$editor = editor_load($filter_format->id());
|
||||
|
||||
// Construct strings to use in the upload validators.
|
||||
$image_upload = $editor->getImageUploadSettings();
|
||||
if (!empty($image_upload['dimensions'])) {
|
||||
$max_dimensions = $image_upload['dimensions']['max_width'] . '×' . $image_upload['dimensions']['max_height'];
|
||||
}
|
||||
else {
|
||||
$max_dimensions = 0;
|
||||
}
|
||||
$max_filesize = min(Bytes::toInt($image_upload['max_size']), file_upload_max_size());
|
||||
|
||||
$existing_file = isset($image_element['data-entity-uuid']) ? \Drupal::entityManager()->loadEntityByUuid('file', $image_element['data-entity-uuid']) : NULL;
|
||||
$fid = $existing_file ? $existing_file->id() : NULL;
|
||||
|
||||
$form['fid'] = array(
|
||||
'#title' => $this->t('Image'),
|
||||
'#type' => 'managed_file',
|
||||
'#upload_location' => $image_upload['scheme'] . '://' . $image_upload['directory'],
|
||||
'#default_value' => $fid ? array($fid) : NULL,
|
||||
'#upload_validators' => array(
|
||||
'file_validate_extensions' => array('gif png jpg jpeg'),
|
||||
'file_validate_size' => array($max_filesize),
|
||||
'file_validate_image_resolution' => array($max_dimensions),
|
||||
),
|
||||
'#required' => TRUE,
|
||||
);
|
||||
|
||||
$form['attributes']['src'] = array(
|
||||
'#title' => $this->t('URL'),
|
||||
'#type' => 'textfield',
|
||||
'#default_value' => isset($image_element['src']) ? $image_element['src'] : '',
|
||||
'#maxlength' => 2048,
|
||||
'#required' => TRUE,
|
||||
);
|
||||
|
||||
// If the editor has image uploads enabled, show a managed_file form item,
|
||||
// otherwise show a (file URL) text form item.
|
||||
if ($image_upload['status']) {
|
||||
$form['attributes']['src']['#access'] = FALSE;
|
||||
$form['attributes']['src']['#required'] = FALSE;
|
||||
}
|
||||
else {
|
||||
$form['fid']['#access'] = FALSE;
|
||||
$form['fid']['#required'] = FALSE;
|
||||
}
|
||||
|
||||
// The alt attribute is *required*, but we allow users to opt-in to empty
|
||||
// alt attributes for the very rare edge cases where that is valid by
|
||||
// specifying two double quotes as the alternative text in the dialog.
|
||||
// However, that *is* stored as an empty alt attribute, so if we're editing
|
||||
// an existing image (which means the src attribute is set) and its alt
|
||||
// attribute is empty, then we show that as two double quotes in the dialog.
|
||||
// @see https://www.drupal.org/node/2307647
|
||||
$alt = isset($image_element['alt']) ? $image_element['alt'] : '';
|
||||
if ($alt === '' && !empty($image_element['src'])) {
|
||||
$alt = '""';
|
||||
}
|
||||
$form['attributes']['alt'] = array(
|
||||
'#title' => $this->t('Alternative text'),
|
||||
'#placeholder' => $this->t('Short description for the visually impaired'),
|
||||
'#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).'),
|
||||
'#default_value' => $alt,
|
||||
'#maxlength' => 2048,
|
||||
);
|
||||
$form['dimensions'] = array(
|
||||
'#type' => 'fieldset',
|
||||
'#title' => $this->t('Image size'),
|
||||
'#attributes' => array('class' => array(
|
||||
'container-inline',
|
||||
'fieldgroup',
|
||||
'form-composite',
|
||||
)),
|
||||
);
|
||||
$form['dimensions']['width'] = array(
|
||||
'#title' => $this->t('Width'),
|
||||
'#title_display' => 'invisible',
|
||||
'#type' => 'number',
|
||||
'#default_value' => isset($image_element['width']) ? $image_element['width'] : '',
|
||||
'#size' => 8,
|
||||
'#maxlength' => 8,
|
||||
'#min' => 1,
|
||||
'#max' => 99999,
|
||||
'#placeholder' => $this->t('width'),
|
||||
'#field_suffix' => ' × ',
|
||||
'#parents' => array('attributes', 'width'),
|
||||
);
|
||||
$form['dimensions']['height'] = array(
|
||||
'#title' => $this->t('Height'),
|
||||
'#title_display' => 'invisible',
|
||||
'#type' => 'number',
|
||||
'#default_value' => isset($image_element['height']) ? $image_element['height'] : '',
|
||||
'#size' => 8,
|
||||
'#maxlength' => 8,
|
||||
'#min' => 1,
|
||||
'#max' => 99999,
|
||||
'#placeholder' => $this->t('height'),
|
||||
'#field_suffix' => $this->t('pixels'),
|
||||
'#parents' => array('attributes', 'height'),
|
||||
);
|
||||
|
||||
// When Drupal core's filter_align is being used, the text editor may
|
||||
// offer the ability to change the alignment.
|
||||
if (isset($image_element['data-align']) && $filter_format->filters('filter_align')->status) {
|
||||
$form['align'] = array(
|
||||
'#title' => $this->t('Align'),
|
||||
'#type' => 'radios',
|
||||
'#options' => array(
|
||||
'none' => $this->t('None'),
|
||||
'left' => $this->t('Left'),
|
||||
'center' => $this->t('Center'),
|
||||
'right' => $this->t('Right'),
|
||||
),
|
||||
'#default_value' => $image_element['data-align'] === '' ? 'none' : $image_element['data-align'],
|
||||
'#wrapper_attributes' => array('class' => array('container-inline')),
|
||||
'#attributes' => array('class' => array('container-inline')),
|
||||
'#parents' => array('attributes', 'data-align'),
|
||||
);
|
||||
}
|
||||
|
||||
// When Drupal core's filter_caption is being used, the text editor may
|
||||
// offer the ability to in-place edit the image's caption: show a toggle.
|
||||
if (isset($image_element['hasCaption']) && $filter_format->filters('filter_caption')->status) {
|
||||
$form['caption'] = array(
|
||||
'#title' => $this->t('Caption'),
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => $image_element['hasCaption'] === 'true',
|
||||
'#parents' => array('attributes', 'hasCaption'),
|
||||
);
|
||||
}
|
||||
|
||||
$form['actions'] = array(
|
||||
'#type' => 'actions',
|
||||
);
|
||||
$form['actions']['save_modal'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Save'),
|
||||
// No regular submit-handler. This form only works via JavaScript.
|
||||
'#submit' => array(),
|
||||
'#ajax' => array(
|
||||
'callback' => '::submitForm',
|
||||
'event' => 'click',
|
||||
),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$response = new AjaxResponse();
|
||||
|
||||
// Convert any uploaded files from the FID values to data-entity-uuid
|
||||
// attributes and set data-entity-type to 'file'.
|
||||
$fid = $form_state->getValue(array('fid', 0));
|
||||
if (!empty($fid)) {
|
||||
$file = file_load($fid);
|
||||
$file_url = file_create_url($file->getFileUri());
|
||||
// Transform absolute image URLs to relative image URLs: prevent problems
|
||||
// on multisite set-ups and prevent mixed content errors.
|
||||
$file_url = file_url_transform_relative($file_url);
|
||||
$form_state->setValue(array('attributes', 'src'), $file_url);
|
||||
$form_state->setValue(array('attributes', 'data-entity-uuid'), $file->uuid());
|
||||
$form_state->setValue(array('attributes', 'data-entity-type'), 'file');
|
||||
}
|
||||
|
||||
// When the alt attribute is set to two double quotes, transform it to the
|
||||
// empty string: two double quotes signify "empty alt attribute". See above.
|
||||
if (trim($form_state->getValue(array('attributes', 'alt'))) === '""') {
|
||||
$form_state->setValue(array('attributes', 'alt'), '');
|
||||
}
|
||||
|
||||
if ($form_state->getErrors()) {
|
||||
unset($form['#prefix'], $form['#suffix']);
|
||||
$form['status_messages'] = [
|
||||
'#type' => 'status_messages',
|
||||
'#weight' => -10,
|
||||
];
|
||||
$response->addCommand(new HtmlCommand('#editor-image-dialog-form', $form));
|
||||
}
|
||||
else {
|
||||
$response->addCommand(new EditorDialogSave($form_state->getValues()));
|
||||
$response->addCommand(new CloseModalDialogCommand());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
102
core/modules/editor/src/Form/EditorLinkDialog.php
Normal file
102
core/modules/editor/src/Form/EditorLinkDialog.php
Normal file
|
@ -0,0 +1,102 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Form\EditorLinkDialog.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Form;
|
||||
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\HtmlCommand;
|
||||
use Drupal\editor\Ajax\EditorDialogSave;
|
||||
use Drupal\Core\Ajax\CloseModalDialogCommand;
|
||||
|
||||
/**
|
||||
* Provides a link dialog for text editors.
|
||||
*/
|
||||
class EditorLinkDialog extends FormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'editor_link_dialog';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param \Drupal\filter\Entity\FilterFormat $filter_format
|
||||
* The filter format for which this dialog corresponds.
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, FilterFormat $filter_format = NULL) {
|
||||
// The default values are set directly from \Drupal::request()->request,
|
||||
// provided by the editor plugin opening the dialog.
|
||||
$user_input = $form_state->getUserInput();
|
||||
$input = isset($user_input['editor_object']) ? $user_input['editor_object'] : array();
|
||||
|
||||
$form['#tree'] = TRUE;
|
||||
$form['#attached']['library'][] = 'editor/drupal.editor.dialog';
|
||||
$form['#prefix'] = '<div id="editor-link-dialog-form">';
|
||||
$form['#suffix'] = '</div>';
|
||||
|
||||
// Everything under the "attributes" key is merged directly into the
|
||||
// generated link tag's attributes.
|
||||
$form['attributes']['href'] = array(
|
||||
'#title' => $this->t('URL'),
|
||||
'#type' => 'textfield',
|
||||
'#default_value' => isset($input['href']) ? $input['href'] : '',
|
||||
'#maxlength' => 2048,
|
||||
);
|
||||
|
||||
$form['attributes']['target'] = array(
|
||||
'#title' => $this->t('Open in new window'),
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => !empty($input['target']),
|
||||
'#return_value' => '_blank',
|
||||
);
|
||||
|
||||
$form['actions'] = array(
|
||||
'#type' => 'actions',
|
||||
);
|
||||
$form['actions']['save_modal'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Save'),
|
||||
// No regular submit-handler. This form only works via JavaScript.
|
||||
'#submit' => array(),
|
||||
'#ajax' => array(
|
||||
'callback' => '::submitForm',
|
||||
'event' => 'click',
|
||||
),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$response = new AjaxResponse();
|
||||
|
||||
if ($form_state->getErrors()) {
|
||||
unset($form['#prefix'], $form['#suffix']);
|
||||
$form['status_messages'] = [
|
||||
'#type' => 'status_messages',
|
||||
'#weight' => -10,
|
||||
];
|
||||
$response->addCommand(new HtmlCommand('#editor-link-dialog-form', $form));
|
||||
}
|
||||
else {
|
||||
$response->addCommand(new EditorDialogSave($form_state->getValues()));
|
||||
$response->addCommand(new CloseModalDialogCommand());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
79
core/modules/editor/src/Plugin/EditorBase.php
Normal file
79
core/modules/editor/src/Plugin/EditorBase.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Plugin\EditorBase.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Plugin;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\editor\Plugin\EditorPluginInterface;
|
||||
|
||||
/**
|
||||
* Defines a base class from which other modules providing editors may extend.
|
||||
*
|
||||
* This class provides default implementations of the EditorPluginInterface so
|
||||
* that classes extending this one do not need to implement every method.
|
||||
*
|
||||
* Plugins extending this class need to define a plugin definition array through
|
||||
* annotation. These definition arrays may be altered through
|
||||
* hook_editor_info_alter(). The definition includes the following keys:
|
||||
*
|
||||
* - id: The unique, system-wide identifier of the text editor. Typically named
|
||||
* the same as the editor library.
|
||||
* - label: The human-readable name of the text editor, translated.
|
||||
* - supports_content_filtering: Whether the editor supports "allowed content
|
||||
* only" filtering.
|
||||
* - supports_inline_editing: Whether the editor supports the inline editing
|
||||
* provided by the Edit module.
|
||||
* - is_xss_safe: Whether this text editor is not vulnerable to XSS attacks.
|
||||
*
|
||||
* A complete sample plugin definition should be defined as in this example:
|
||||
*
|
||||
* @code
|
||||
* @Editor(
|
||||
* id = "myeditor",
|
||||
* label = @Translation("My Editor"),
|
||||
* supports_content_filtering = FALSE,
|
||||
* supports_inline_editing = FALSE,
|
||||
* is_xss_safe = FALSE
|
||||
* )
|
||||
* @endcode
|
||||
*
|
||||
* @see \Drupal\editor\Annotation\Editor
|
||||
* @see \Drupal\editor\Plugin\EditorPluginInterface
|
||||
* @see \Drupal\editor\Plugin\EditorManager
|
||||
* @see plugin_api
|
||||
*/
|
||||
abstract class EditorBase extends PluginBase implements EditorPluginInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultSettings() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsFormValidate(array $form, FormStateInterface $form_state) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsFormSubmit(array $form, FormStateInterface $form_state) {
|
||||
}
|
||||
|
||||
}
|
104
core/modules/editor/src/Plugin/EditorManager.php
Normal file
104
core/modules/editor/src/Plugin/EditorManager.php
Normal file
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Plugin\EditorManager.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Plugin;
|
||||
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
|
||||
/**
|
||||
* Configurable text editor manager.
|
||||
*
|
||||
* @see \Drupal\editor\Annotation\Editor
|
||||
* @see \Drupal\editor\Plugin\EditorPluginInterface
|
||||
* @see \Drupal\editor\Plugin\EditorBase
|
||||
* @see plugin_api
|
||||
*/
|
||||
class EditorManager extends DefaultPluginManager {
|
||||
|
||||
/**
|
||||
* Constructs an EditorManager object.
|
||||
*
|
||||
* @param \Traversable $namespaces
|
||||
* An object that implements \Traversable which contains the root paths
|
||||
* keyed by the corresponding namespace to look for plugin implementations.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* Cache backend instance to use.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler to invoke the alter hook with.
|
||||
*/
|
||||
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
|
||||
parent::__construct('Plugin/Editor', $namespaces, $module_handler, 'Drupal\editor\Plugin\EditorPluginInterface', 'Drupal\editor\Annotation\Editor');
|
||||
$this->alterInfo('editor_info');
|
||||
$this->setCacheBackend($cache_backend, 'editor_plugins');
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates a key-value pair of available text editors.
|
||||
*
|
||||
* @return array
|
||||
* An array of translated text editor labels, keyed by ID.
|
||||
*/
|
||||
public function listOptions() {
|
||||
$options = array();
|
||||
foreach ($this->getDefinitions() as $key => $definition) {
|
||||
$options[$key] = $definition['label'];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves text editor libraries and JavaScript settings.
|
||||
*
|
||||
* @param array $format_ids
|
||||
* An array of format IDs as returned by array_keys(filter_formats()).
|
||||
*
|
||||
* @return array
|
||||
* An array of attachments, for use with #attached.
|
||||
*
|
||||
* @see drupal_process_attached()
|
||||
*/
|
||||
public function getAttachments(array $format_ids) {
|
||||
$attachments = array('library' => array());
|
||||
|
||||
$settings = array();
|
||||
foreach ($format_ids as $format_id) {
|
||||
$editor = editor_load($format_id);
|
||||
if (!$editor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$plugin = $this->createInstance($editor->getEditor());
|
||||
$plugin_definition = $plugin->getPluginDefinition();
|
||||
|
||||
// Libraries.
|
||||
$attachments['library'] = array_merge($attachments['library'], $plugin->getLibraries($editor));
|
||||
|
||||
// Format-specific JavaScript settings.
|
||||
$settings['editor']['formats'][$format_id] = array(
|
||||
'format' => $format_id,
|
||||
'editor' => $editor->getEditor(),
|
||||
'editorSettings' => $plugin->getJSSettings($editor),
|
||||
'editorSupportsContentFiltering' => $plugin_definition['supports_content_filtering'],
|
||||
'isXssSafe' => $plugin_definition['is_xss_safe'],
|
||||
);
|
||||
}
|
||||
|
||||
// Allow other modules to alter all JavaScript settings.
|
||||
$this->moduleHandler->alter('editor_js_settings', $settings);
|
||||
|
||||
if (empty($attachments['library']) && empty($settings)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$attachments['drupalSettings'] = $settings;
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
}
|
122
core/modules/editor/src/Plugin/EditorPluginInterface.php
Normal file
122
core/modules/editor/src/Plugin/EditorPluginInterface.php
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Plugin\EditorPluginInterface.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Plugin;
|
||||
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines an interface for configurable text editors.
|
||||
*
|
||||
* Modules implementing this interface may want to extend the EditorBase class,
|
||||
* which provides default implementations of each method where appropriate.
|
||||
*
|
||||
* @see \Drupal\editor\Annotation\Editor
|
||||
* @see \Drupal\editor\Plugin\EditorBase
|
||||
* @see \Drupal\editor\Plugin\EditorManager
|
||||
* @see plugin_api
|
||||
*/
|
||||
interface EditorPluginInterface extends PluginInspectionInterface {
|
||||
|
||||
/**
|
||||
* Returns the default settings for this configurable text editor.
|
||||
*
|
||||
* @return array
|
||||
* An array of settings as they would be stored by a configured text editor
|
||||
* entity (\Drupal\editor\Entity\Editor).
|
||||
*/
|
||||
public function getDefaultSettings();
|
||||
|
||||
/**
|
||||
* Returns a settings form to configure this text editor.
|
||||
*
|
||||
* If the editor's behavior depends on extensive options and/or external data,
|
||||
* then the implementing module can choose to provide a separate, global
|
||||
* configuration page rather than per-text-format settings. In that case, this
|
||||
* form should provide a link to the separate settings page.
|
||||
*
|
||||
* @param array $form
|
||||
* An empty form array to be populated with a configuration form, if any.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The state of the entire filter administration form.
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return array
|
||||
* A render array for the settings form.
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor);
|
||||
|
||||
/**
|
||||
* Validates the settings form for an editor.
|
||||
*
|
||||
* The contents of the editor settings are located in
|
||||
* $form_state->getValue(array('editor', 'settings')). Calls to $form_state->setError()
|
||||
* should reflect this location in the settings form.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public function settingsFormValidate(array $form, FormStateInterface $form_state);
|
||||
|
||||
/**
|
||||
* Modifies any values in the form state to prepare them for saving.
|
||||
*
|
||||
* Values in $form_state->getValue(array('editor', 'settings')) are saved by
|
||||
* Editor module in editor_form_filter_admin_format_submit().
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public function settingsFormSubmit(array $form, FormStateInterface $form_state);
|
||||
|
||||
/**
|
||||
* Returns JavaScript settings to be attached.
|
||||
*
|
||||
* Most text editors use JavaScript to provide a WYSIWYG or toolbar on the
|
||||
* client-side interface. This method can be used to convert internal settings
|
||||
* of the text editor into JavaScript variables that will be accessible when
|
||||
* the text editor is loaded.
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return array
|
||||
* An array of settings that will be added to the page for use by this text
|
||||
* editor's JavaScript integration.
|
||||
*
|
||||
* @see drupal_process_attached()
|
||||
* @see EditorManager::getAttachments()
|
||||
*/
|
||||
public function getJSSettings(Editor $editor);
|
||||
|
||||
/**
|
||||
* Returns libraries to be attached.
|
||||
*
|
||||
* Because this is a method, plugins can dynamically choose to attach a
|
||||
* different library for different configurations, instead of being forced to
|
||||
* always use the same method.
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return array
|
||||
* An array of libraries that will be added to the page for use by this text
|
||||
* editor.
|
||||
*
|
||||
* @see drupal_process_attached()
|
||||
* @see EditorManager::getAttachments()
|
||||
*/
|
||||
public function getLibraries(Editor $editor);
|
||||
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Plugin\Filter\EditorFileReference.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Plugin\Filter;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\filter\FilterProcessResult;
|
||||
use Drupal\filter\Plugin\FilterBase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a filter to track images uploaded via a Text Editor.
|
||||
*
|
||||
* Passes the text unchanged, but associates the cache tags of referenced files.
|
||||
*
|
||||
* @Filter(
|
||||
* id = "editor_file_reference",
|
||||
* title = @Translation("Track images uploaded via a Text Editor"),
|
||||
* description = @Translation("Ensures that the latest versions of images uploaded via a Text Editor are displayed."),
|
||||
* type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
|
||||
* )
|
||||
*/
|
||||
class EditorFileReference extends FilterBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* An entity manager object.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityManagerInterface
|
||||
*/
|
||||
protected $entityManager;
|
||||
|
||||
/**
|
||||
* Constructs a \Drupal\editor\Plugin\Filter\EditorFileReference object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
|
||||
* An entity manager object.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager) {
|
||||
$this->entityManager = $entity_manager;
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
static public function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('entity.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process($text, $langcode) {
|
||||
$result = new FilterProcessResult($text);
|
||||
|
||||
if (stristr($text, 'data-entity-type="file"') !== FALSE) {
|
||||
$dom = Html::load($text);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$processed_uuids = array();
|
||||
foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid]') as $node) {
|
||||
$uuid = $node->getAttribute('data-entity-uuid');
|
||||
// Only process the first occurrence of each file UUID.
|
||||
if (!isset($processed_uuids[$uuid])) {
|
||||
$processed_uuids[$uuid] = TRUE;
|
||||
|
||||
$file = $this->entityManager->loadEntityByUuid('file', $uuid);
|
||||
if ($file) {
|
||||
$result->addCacheTags($file->getCacheTags());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
98
core/modules/editor/src/Plugin/InPlaceEditor/Editor.php
Normal file
98
core/modules/editor/src/Plugin/InPlaceEditor/Editor.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Plugin\InPlaceEditor\Editor.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Plugin\InPlaceEditor;
|
||||
|
||||
use Drupal\Component\Plugin\PluginBase;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\quickedit\Plugin\InPlaceEditorInterface;
|
||||
use Drupal\filter\Plugin\FilterInterface;
|
||||
|
||||
/**
|
||||
* Defines the formatted text in-place editor.
|
||||
*
|
||||
* @InPlaceEditor(
|
||||
* id = "editor",
|
||||
* alternativeTo = {"plain_text"}
|
||||
* )
|
||||
*/
|
||||
class Editor extends PluginBase implements InPlaceEditorInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCompatible(FieldItemListInterface $items) {
|
||||
$field_definition = $items->getFieldDefinition();
|
||||
|
||||
// This editor is incompatible with multivalued fields.
|
||||
if ($field_definition->getFieldStorageDefinition()->getCardinality() != 1) {
|
||||
return FALSE;
|
||||
}
|
||||
// This editor is compatible with formatted ("rich") text fields; but only
|
||||
// if there is a currently active text format, that text format has an
|
||||
// associated editor and that editor supports inline editing.
|
||||
elseif (in_array($field_definition->getType(), array('text', 'text_long', 'text_with_summary'), TRUE)) {
|
||||
if ($editor = editor_load($items[0]->format)) {
|
||||
$definition = \Drupal::service('plugin.manager.editor')->getDefinition($editor->getEditor());
|
||||
if ($definition['supports_inline_editing'] === TRUE) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getMetadata(FieldItemListInterface $items) {
|
||||
$format_id = $items[0]->format;
|
||||
$metadata['format'] = $format_id;
|
||||
$metadata['formatHasTransformations'] = $this->textFormatHasTransformationFilters($format_id);
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the text format has transformation filters.
|
||||
*/
|
||||
protected function textFormatHasTransformationFilters($format_id) {
|
||||
$format = entity_load('filter_format', $format_id);
|
||||
return (bool) count(array_intersect(array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE), $format->getFiltertypes()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAttachments() {
|
||||
$user = \Drupal::currentUser();
|
||||
|
||||
$user_format_ids = array_keys(filter_formats($user));
|
||||
$manager = \Drupal::service('plugin.manager.editor');
|
||||
$definitions = $manager->getDefinitions();
|
||||
|
||||
// Filter the current user's formats to those that support inline editing.
|
||||
$formats = array();
|
||||
foreach ($user_format_ids as $format_id) {
|
||||
if ($editor = editor_load($format_id)) {
|
||||
$editor_id = $editor->getEditor();
|
||||
if (isset($definitions[$editor_id]['supports_inline_editing']) && $definitions[$editor_id]['supports_inline_editing'] === TRUE) {
|
||||
$formats[] = $format_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the attachments for all text editors that the user might use.
|
||||
$attachments = $manager->getAttachments($formats);
|
||||
|
||||
// Also include editor.module's formatted text editor.
|
||||
$attachments['library'][] = 'editor/quickedit.inPlaceEditor.formattedText';
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
}
|
173
core/modules/editor/src/Tests/EditorAdminTest.php
Normal file
173
core/modules/editor/src/Tests/EditorAdminTest.php
Normal file
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Tests\EditorAdminTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* Tests administration of text editors.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorAdminTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('filter', 'editor');
|
||||
|
||||
/**
|
||||
* A user with the 'administer filters' permission.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Add text format.
|
||||
$filtered_html_format = entity_create('filter_format', array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
|
||||
// Create admin user.
|
||||
$this->adminUser = $this->drupalCreateUser(array('administer filters'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests an existing format without any editors available.
|
||||
*/
|
||||
public function testNoEditorAvailable() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$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');
|
||||
$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>.
|
||||
$select = $this->xpath('//select[@name="editor[editor]"]');
|
||||
$select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]');
|
||||
$options = $this->xpath('//select[@name="editor[editor]"]/option');
|
||||
$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.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests adding a text editor to an existing text format.
|
||||
*/
|
||||
public function testAddEditorToExistingFormat() {
|
||||
$this->enableUnicornEditor();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
|
||||
$edit = $this->selectUnicornEditor();
|
||||
// Configure Unicorn Editor's setting to another value.
|
||||
$edit['editor[settings][ponies_too]'] = FALSE;
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
$this->verifyUnicornEditorConfiguration('filtered_html', FALSE);
|
||||
|
||||
// Switch back to 'None' and check the Unicorn Editor's settings are gone.
|
||||
$edit = array(
|
||||
'editor[editor]' => '',
|
||||
);
|
||||
$this->drupalPostAjaxForm(NULL, $edit, 'editor_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.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests adding a text editor to a new text format.
|
||||
*/
|
||||
public function testAddEditorToNewFormat() {
|
||||
$this->enableUnicornEditor();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/config/content/formats/add');
|
||||
// Configure the text format name.
|
||||
$edit = array(
|
||||
'name' => 'Monocerus',
|
||||
'format' => 'monocerus',
|
||||
);
|
||||
$edit += $this->selectUnicornEditor();
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
$this->verifyUnicornEditorConfiguration($edit['format']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the unicorn editor.
|
||||
*/
|
||||
protected function enableUnicornEditor() {
|
||||
\Drupal::service('module_installer')->install(array('editor_test'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests and selects the unicorn editor.
|
||||
*
|
||||
* @return array
|
||||
* Returns an edit array containing the values to be posted.
|
||||
*/
|
||||
protected function selectUnicornEditor() {
|
||||
// Verify the <select> when a text editor is available.
|
||||
$select = $this->xpath('//select[@name="editor[editor]"]');
|
||||
$select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]');
|
||||
$options = $this->xpath('//select[@name="editor[editor]"]/option');
|
||||
$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.');
|
||||
// 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.');
|
||||
|
||||
// Select the "Unicorn Editor" editor and click the "Configure" button.
|
||||
$edit = array(
|
||||
'editor[editor]' => 'unicorn',
|
||||
);
|
||||
$this->drupalPostAjaxForm(NULL, $edit, 'editor_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.");
|
||||
|
||||
return $edit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies unicorn editor configuration.
|
||||
*
|
||||
* @param string $format_id
|
||||
* The format machine name.
|
||||
* @param bool $ponies_too
|
||||
* The expected value of the ponies_too setting.
|
||||
*/
|
||||
protected function verifyUnicornEditorConfiguration($format_id, $ponies_too = TRUE) {
|
||||
$editor = editor_load($format_id);
|
||||
$settings = $editor->getSettings();
|
||||
$this->assertIdentical($editor->getEditor(), 'unicorn', 'The text editor is configured correctly.');
|
||||
$this->assertIdentical($settings['ponies_too'], $ponies_too, 'The text editor settings are stored correctly.');
|
||||
$this->drupalGet('admin/config/content/formats/manage/'. $format_id);
|
||||
$select = $this->xpath('//select[@name="editor[editor]"]');
|
||||
$select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]');
|
||||
$options = $this->xpath('//select[@name="editor[editor]"]/option');
|
||||
$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.');
|
||||
}
|
||||
|
||||
}
|
120
core/modules/editor/src/Tests/EditorFileReferenceFilterTest.php
Normal file
120
core/modules/editor/src/Tests/EditorFileReferenceFilterTest.php
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Tests\EditorFileReferenceFilterTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\simpletest\KernelTestBase;
|
||||
use Drupal\filter\FilterPluginCollection;
|
||||
|
||||
/**
|
||||
* Tests Editor module's file reference filter.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorFileReferenceFilterTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('system', 'filter', 'editor', 'field', 'file', 'user');
|
||||
|
||||
/**
|
||||
* @var \Drupal\filter\Plugin\FilterInterface[]
|
||||
*/
|
||||
protected $filters;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installConfig(array('system'));
|
||||
$this->installEntitySchema('file');
|
||||
$this->installSchema('file', array('file_usage'));
|
||||
|
||||
$manager = $this->container->get('plugin.manager.filter');
|
||||
$bag = new FilterPluginCollection($manager, array());
|
||||
$this->filters = $bag->getAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the editor file reference filter.
|
||||
*/
|
||||
function testEditorFileReferenceFilter() {
|
||||
$filter = $this->filters['editor_file_reference'];
|
||||
|
||||
$test = function($input) use ($filter) {
|
||||
return $filter->process($input, 'und');
|
||||
};
|
||||
|
||||
file_put_contents('public://llama.jpg', $this->randomMachineName());
|
||||
$image = entity_create('file', array('uri' => 'public://llama.jpg'));
|
||||
$image->save();
|
||||
$id = $image->id();
|
||||
$uuid = $image->uuid();
|
||||
$cache_tag = ['file:' . $id];
|
||||
|
||||
file_put_contents('public://alpaca.jpg', $this->randomMachineName());
|
||||
$image_2 = entity_create('file', array('uri' => 'public://alpaca.jpg'));
|
||||
$image_2->save();
|
||||
$id_2 = $image_2->id();
|
||||
$uuid_2 = $image_2->uuid();
|
||||
$cache_tag_2 = ['file:' . $id_2];
|
||||
|
||||
$this->pass('No data-entity-type and no data-entity-uuid attribute.');
|
||||
$input = '<img src="llama.jpg" />';
|
||||
$output = $test($input);
|
||||
$this->assertIdentical($input, $output->getProcessedText());
|
||||
|
||||
$this->pass('A non-file data-entity-type attribute value.');
|
||||
$input = '<img src="llama.jpg" data-entity-type="invalid-entity-type-value" data-entity-uuid="' . $uuid . '" />';
|
||||
$output = $test($input);
|
||||
$this->assertIdentical($input, $output->getProcessedText());
|
||||
|
||||
$this->pass('One data-entity-uuid attribute.');
|
||||
$input = '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" />';
|
||||
$output = $test($input);
|
||||
$this->assertIdentical($input, $output->getProcessedText());
|
||||
$this->assertEqual($cache_tag, $output->getCacheTags());
|
||||
|
||||
$this->pass('One data-entity-uuid attribute with odd capitalization.');
|
||||
$input = '<img src="llama.jpg" data-entity-type="file" DATA-entity-UUID = "' . $uuid . '" />';
|
||||
$output = $test($input);
|
||||
$this->assertIdentical($input, $output->getProcessedText());
|
||||
$this->assertEqual($cache_tag, $output->getCacheTags());
|
||||
|
||||
$this->pass('One data-entity-uuid attribute on a non-image tag.');
|
||||
$input = '<video src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" />';
|
||||
$output = $test($input);
|
||||
$this->assertIdentical($input, $output->getProcessedText());
|
||||
$this->assertEqual($cache_tag, $output->getCacheTags());
|
||||
|
||||
$this->pass('One data-entity-uuid attribute with an invalid value.');
|
||||
$input = '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="invalid-' . $uuid . '" />';
|
||||
$output = $test($input);
|
||||
$this->assertIdentical($input, $output->getProcessedText());
|
||||
$this->assertEqual(array(), $output->getCacheTags());
|
||||
|
||||
$this->pass('Two different data-entity-uuid attributes.');
|
||||
$input = '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" />';
|
||||
$input .= '<img src="alpaca.jpg" data-entity-type="file" data-entity-uuid="' . $uuid_2 . '" />';
|
||||
$output = $test($input);
|
||||
$this->assertIdentical($input, $output->getProcessedText());
|
||||
$this->assertEqual(Cache::mergeTags($cache_tag, $cache_tag_2), $output->getCacheTags());
|
||||
|
||||
$this->pass('Two identical data-entity-uuid attributes.');
|
||||
$input = '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" />';
|
||||
$input .= '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" />';
|
||||
$output = $test($input);
|
||||
$this->assertIdentical($input, $output->getProcessedText());
|
||||
$this->assertEqual($cache_tag, $output->getCacheTags());
|
||||
}
|
||||
|
||||
}
|
134
core/modules/editor/src/Tests/EditorFileUsageTest.php
Normal file
134
core/modules/editor/src/Tests/EditorFileUsageTest.php
Normal file
|
@ -0,0 +1,134 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Tests\EditorFileUsageTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\system\Tests\Entity\EntityUnitTestBase;
|
||||
|
||||
/**
|
||||
* Tests tracking of file usage by the Text Editor module.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorFileUsageTest extends EntityUnitTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('editor', 'editor_test', 'node', 'file');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
$this->installEntitySchema('file');
|
||||
$this->installSchema('node', array('node_access'));
|
||||
$this->installSchema('file', array('file_usage'));
|
||||
$this->installConfig(['node']);
|
||||
|
||||
// Add text formats.
|
||||
$filtered_html_format = entity_create('filter_format', array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
|
||||
// Set up text editor.
|
||||
$editor = entity_create('editor', array(
|
||||
'format' => 'filtered_html',
|
||||
'editor' => 'unicorn',
|
||||
));
|
||||
$editor->save();
|
||||
|
||||
// Create a node type for testing.
|
||||
$type = entity_create('node_type', array('type' => 'page', 'name' => 'page'));
|
||||
$type->save();
|
||||
node_add_body_field($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the configurable text editor manager.
|
||||
*/
|
||||
public function testEditorEntityHooks() {
|
||||
$image = entity_create('file');
|
||||
$image->setFileUri('core/misc/druplicon.png');
|
||||
$image->setFilename(drupal_basename($image->getFileUri()));
|
||||
$image->save();
|
||||
$file_usage = $this->container->get('file.usage');
|
||||
$this->assertIdentical(array(), $file_usage->listUsage($image), 'The image has zero usages.');
|
||||
|
||||
$body_value = '<p>Hello, world!</p><img src="awesome-llama.jpg" data-entity-type="file" data-entity-uuid="' . $image->uuid() . '" />';
|
||||
// Test handling of an invalid data-entity-uuid attribute.
|
||||
$body_value .= '<img src="awesome-llama.jpg" data-entity-type="file" data-entity-uuid="invalid-entity-uuid-value" />';
|
||||
// Test handling of an invalid data-entity-type attribute.
|
||||
$body_value .= '<img src="awesome-llama.jpg" data-entity-type="invalid-entity-type-value" data-entity-uuid="' . $image->uuid() . '" />';
|
||||
// Test handling of a non-existing UUID.
|
||||
$body_value .= '<img src="awesome-llama.jpg" data-entity-type="file" data-entity-uuid="30aac704-ba2c-40fc-b609-9ed121aa90f4" />';
|
||||
// Test editor_entity_insert(): increment.
|
||||
$this->createUser();
|
||||
$node = entity_create('node', array(
|
||||
'type' => 'page',
|
||||
'title' => 'test',
|
||||
'body' => array(
|
||||
'value' => $body_value,
|
||||
'format' => 'filtered_html',
|
||||
),
|
||||
'uid' => 1,
|
||||
));
|
||||
$node->save();
|
||||
$this->assertIdentical(array('editor' => array('node' => array(1 => '1'))), $file_usage->listUsage($image), 'The image has 1 usage.');
|
||||
|
||||
// Test editor_entity_update(): increment, twice, by creating new revisions.
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->save();
|
||||
$second_revision_id = $node->getRevisionId();
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->save();
|
||||
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.');
|
||||
|
||||
// Test hook_entity_update(): decrement, by modifying the last revision:
|
||||
// remove the data-entity-type attribute from the body field.
|
||||
$body = $node->get('body')->first()->get('value');
|
||||
$original_value = $body->getValue();
|
||||
$new_value = str_replace('data-entity-type', 'data-entity-type-modified', $original_value);
|
||||
$body->setValue($new_value);
|
||||
$node->save();
|
||||
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.');
|
||||
|
||||
// Test editor_entity_update(): increment again by creating a new revision:
|
||||
// read the data- attributes to the body field.
|
||||
$node->setNewRevision(TRUE);
|
||||
$node->get('body')->first()->get('value')->setValue($original_value);
|
||||
$node->save();
|
||||
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.');
|
||||
|
||||
// Test hook_entity_update(): decrement, by modifying the last revision:
|
||||
// remove the data-entity-uuid attribute from the body field.
|
||||
$body = $node->get('body')->first()->get('value');
|
||||
$new_value = str_replace('data-entity-uuid', 'data-entity-uuid-modified', $original_value);
|
||||
$body->setValue($new_value);
|
||||
$node->save();
|
||||
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.');
|
||||
|
||||
// Test hook_entity_update(): increment, by modifying the last revision:
|
||||
// read the data- attributes to the body field.
|
||||
$node->get('body')->first()->get('value')->setValue($original_value);
|
||||
$node->save();
|
||||
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.');
|
||||
|
||||
// Test editor_entity_revision_delete(): decrement, by deleting a revision.
|
||||
entity_revision_delete('node', $second_revision_id);
|
||||
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.');
|
||||
|
||||
// Test editor_entity_delete().
|
||||
$node->delete();
|
||||
$this->assertIdentical(array(), $file_usage->listUsage($image), 'The image has zero usages again.');
|
||||
}
|
||||
|
||||
}
|
289
core/modules/editor/src/Tests/EditorLoadingTest.php
Normal file
289
core/modules/editor/src/Tests/EditorLoadingTest.php
Normal file
|
@ -0,0 +1,289 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Tests\EditorLoadingTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\Core\Entity\Entity;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* Tests loading of text editors.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorLoadingTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('filter', 'editor', 'editor_test', 'node');
|
||||
|
||||
/**
|
||||
* An untrusted user, with access to the 'plain_text' format.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $untrustedUser;
|
||||
|
||||
/**
|
||||
* A normal user with additional access to the 'filtered_html' format.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $normalUser;
|
||||
|
||||
/**
|
||||
* A privileged user with additional access to the 'full_html' format.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $privilegedUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Let there be T-rex.
|
||||
\Drupal::state()->set('editor_test_give_me_a_trex_thanks', TRUE);
|
||||
\Drupal::service('plugin.manager.editor')->clearCachedDefinitions();
|
||||
|
||||
// Add text formats.
|
||||
$filtered_html_format = entity_create('filter_format', array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
$full_html_format = entity_create('filter_format', array(
|
||||
'format' => 'full_html',
|
||||
'name' => 'Full HTML',
|
||||
'weight' => 1,
|
||||
'filters' => array(),
|
||||
));
|
||||
$full_html_format->save();
|
||||
|
||||
// Create article node type.
|
||||
$this->drupalCreateContentType(array(
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
));
|
||||
|
||||
// Create page node type, but remove the body.
|
||||
$this->drupalCreateContentType(array(
|
||||
'type' => 'page',
|
||||
'name' => 'Page',
|
||||
));
|
||||
$body = FieldConfig::loadByName('node', 'page', 'body');
|
||||
$body->delete();
|
||||
|
||||
// Create a formatted text field, which uses an <input type="text">.
|
||||
FieldStorageConfig::create(array(
|
||||
'field_name' => 'field_text',
|
||||
'entity_type' => 'node',
|
||||
'type' => 'text',
|
||||
))->save();
|
||||
|
||||
FieldConfig::create(array(
|
||||
'field_name' => 'field_text',
|
||||
'entity_type' => 'node',
|
||||
'label' => 'Textfield',
|
||||
'bundle' => 'page',
|
||||
))->save();
|
||||
|
||||
entity_get_form_display('node', 'page', 'default')
|
||||
->setComponent('field_text')
|
||||
->save();
|
||||
|
||||
// Create 3 users, each with access to different text formats.
|
||||
$this->untrustedUser = $this->drupalCreateUser(array('create article content', 'edit any article content'));
|
||||
$this->normalUser = $this->drupalCreateUser(array('create article content', 'edit any article content', 'use text format filtered_html'));
|
||||
$this->privilegedUser = $this->drupalCreateUser(array('create article content', 'edit any article content', 'create page content', 'edit any page content', 'use text format filtered_html', 'use text format full_html'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests loading of text editors.
|
||||
*/
|
||||
public function testLoading() {
|
||||
// Only associate a text editor with the "Full HTML" text format.
|
||||
$editor = entity_create('editor', array(
|
||||
'format' => 'full_html',
|
||||
'editor' => 'unicorn',
|
||||
'image_upload' => array(
|
||||
'status' => FALSE,
|
||||
'scheme' => file_default_scheme(),
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => '',
|
||||
'max_dimensions' => array('width' => '', 'height' => ''),
|
||||
)
|
||||
));
|
||||
$editor->save();
|
||||
|
||||
// The normal user:
|
||||
// - has access to 2 text formats;
|
||||
// - doesn't have access to the full_html text format, so: no text editor.
|
||||
$this->drupalLogin($this->normalUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
list( , $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$this->assertFalse($editor_settings_present, 'No Text Editor module settings.');
|
||||
$this->assertFalse($editor_js_present, 'No Text Editor JavaScript.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
$this->assertTrue(count($format_selector) === 0, 'No text format selector exists on the page because the user only has access to a single format.');
|
||||
$this->drupalLogout($this->normalUser);
|
||||
|
||||
// The privileged user:
|
||||
// - has access to 2 text formats (and the fallback format);
|
||||
// - does have access to the full_html text format, so: Unicorn text editor.
|
||||
$this->drupalLogin($this->privilegedUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$expected = array('formats' => array('full_html' => array(
|
||||
'format' => 'full_html',
|
||||
'editor' => 'unicorn',
|
||||
'editorSettings' => array('ponyModeEnabled' => TRUE),
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
)));
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertIdentical($expected, $settings['editor'], "Text Editor module's JavaScript settings on the page are correct.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
$this->assertTrue(count($format_selector) === 1, 'A single text format selector exists on the page.');
|
||||
$specific_format_selector = $this->xpath('//select[contains(@class, "filter-list") and @data-editor-for="edit-body-0-value"]');
|
||||
$this->assertTrue(count($specific_format_selector) === 1, 'A single text format selector exists on the page and has a "data-editor-for" attribute with the correct value.');
|
||||
|
||||
// Load the editor image dialog form and make sure it does not fatal.
|
||||
$this->drupalGet('editor/dialog/image/full_html');
|
||||
$this->assertResponse(200);
|
||||
|
||||
$this->drupalLogout($this->privilegedUser);
|
||||
|
||||
// Also associate a text editor with the "Plain Text" text format.
|
||||
$editor = entity_create('editor', array(
|
||||
'format' => 'plain_text',
|
||||
'editor' => 'unicorn',
|
||||
));
|
||||
$editor->save();
|
||||
|
||||
// The untrusted user:
|
||||
// - has access to 1 text format (plain_text);
|
||||
// - has access to the plain_text text format, so: Unicorn text editor.
|
||||
$this->drupalLogin($this->untrustedUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$expected = array('formats' => array('plain_text' => array(
|
||||
'format' => 'plain_text',
|
||||
'editor' => 'unicorn',
|
||||
'editorSettings' => array('ponyModeEnabled' => TRUE),
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
)));
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertIdentical($expected, $settings['editor'], "Text Editor module's JavaScript settings on the page are correct.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
$this->assertTrue(count($format_selector) === 0, 'No text format selector exists on the page.');
|
||||
$hidden_input = $this->xpath('//input[@type="hidden" and @value="plain_text" and @data-editor-for="edit-body-0-value"]');
|
||||
$this->assertTrue(count($hidden_input) === 1, 'A single text format hidden input exists on the page and has a "data-editor-for" attribute with the correct value.');
|
||||
|
||||
// Create an "article" node that uses the full_html text format, then try
|
||||
// to let the untrusted user edit it.
|
||||
$this->drupalCreateNode(array(
|
||||
'type' => 'article',
|
||||
'body' => array(
|
||||
array('value' => $this->randomMachineName(32), 'format' => 'full_html')
|
||||
),
|
||||
));
|
||||
|
||||
// The untrusted user tries to edit content that is written in a text format
|
||||
// that (s)he is not allowed to use. The editor is still loaded. CKEditor,
|
||||
// for example, supports being loaded in a disabled state.
|
||||
$this->drupalGet('node/1/edit');
|
||||
list( , $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$this->assertTrue($editor_settings_present, 'Text Editor module settings.');
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
$this->assertFieldByXPath('//textarea[@id="edit-body-0-value" and @disabled="disabled"]', t('This field has been disabled because you do not have sufficient permissions to edit it.'), 'Text format access denied message found.');
|
||||
$this->assertTrue(count($format_selector) === 0, 'No text format selector exists on the page.');
|
||||
$hidden_input = $this->xpath('//input[@type="hidden" and contains(@class, "editor")]');
|
||||
$this->assertTrue(count($hidden_input) === 0, 'A single text format hidden input does not exist on the page.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test supported element types.
|
||||
*/
|
||||
public function testSupportedElementTypes() {
|
||||
// Associate the unicorn text editor with the "Full HTML" text format.
|
||||
$editor = entity_create('editor', array(
|
||||
'format' => 'full_html',
|
||||
'editor' => 'unicorn',
|
||||
'image_upload' => array(
|
||||
'status' => FALSE,
|
||||
'scheme' => file_default_scheme(),
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => '',
|
||||
'max_dimensions' => array('width' => '', 'height' => ''),
|
||||
)
|
||||
));
|
||||
$editor->save();
|
||||
|
||||
// Create an "page" node that uses the full_html text format.
|
||||
$this->drupalCreateNode(array(
|
||||
'type' => 'page',
|
||||
'field_text' => array(
|
||||
array('value' => $this->randomMachineName(32), 'format' => 'full_html')
|
||||
),
|
||||
));
|
||||
|
||||
// Assert the unicorn editor works with textfields.
|
||||
$this->drupalLogin($this->privilegedUser);
|
||||
$this->drupalGet('node/1/edit');
|
||||
list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
$this->assertTrue(count($field) === 1, 'A text field exists.');
|
||||
$this->assertTrue(count($format_selector) === 1, 'A single text format selector exists on the page.');
|
||||
$specific_format_selector = $this->xpath('//select[contains(@class, "filter-list") and contains(@class, "editor") and @data-editor-for="edit-field-text-0-value"]');
|
||||
$this->assertTrue(count($specific_format_selector) === 1, 'A single text format selector exists on the page and has the "editor" class and a "data-editor-for" attribute with the correct value.');
|
||||
|
||||
// Associate the trex text editor with the "Full HTML" text format.
|
||||
$editor->delete();
|
||||
entity_create('editor', array(
|
||||
'format' => 'full_html',
|
||||
'editor' => 'trex',
|
||||
))->save();
|
||||
|
||||
$this->drupalGet('node/1/edit');
|
||||
list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
|
||||
$this->assertFalse($editor_settings_present, "Text Editor module's JavaScript settings are not on the page.");
|
||||
$this->assertFalse($editor_js_present, 'Text Editor JavaScript is not present.');
|
||||
$this->assertTrue(count($field) === 1, 'A text field exists.');
|
||||
$this->assertTrue(count($format_selector) === 1, 'A single text format selector exists on the page.');
|
||||
$specific_format_selector = $this->xpath('//select[contains(@class, "filter-list") and contains(@class, "editor") and @data-editor-for="edit-field-text-0-value"]');
|
||||
$this->assertFalse(count($specific_format_selector) === 1, 'A single text format selector exists on the page and has the "editor" class and a "data-editor-for" attribute with the correct value.');
|
||||
}
|
||||
|
||||
protected function getThingsToCheck($field_name, $type = 'textarea') {
|
||||
$settings = $this->getDrupalSettings();
|
||||
return array(
|
||||
// JavaScript settings.
|
||||
$settings,
|
||||
// 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,
|
||||
// Body field.
|
||||
$this->xpath('//' . $type . '[@id="edit-' . $field_name . '-0-value"]'),
|
||||
// Format selector.
|
||||
$this->xpath('//select[contains(@class, "filter-list")]'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
116
core/modules/editor/src/Tests/EditorManagerTest.php
Normal file
116
core/modules/editor/src/Tests/EditorManagerTest.php
Normal file
|
@ -0,0 +1,116 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Tests\EditorManagerTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\simpletest\KernelTestBase;
|
||||
use Drupal\editor\Plugin\EditorManager;
|
||||
|
||||
/**
|
||||
* Tests detection of text editors and correct generation of attachments.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorManagerTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('system', 'user', 'filter', 'editor');
|
||||
|
||||
/**
|
||||
* The manager for text editor plugins.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected $editorManager;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Install the Filter module.
|
||||
$this->installSchema('system', 'url_alias');
|
||||
|
||||
// Add text formats.
|
||||
$filtered_html_format = entity_create('filter_format', array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
$full_html_format = entity_create('filter_format', array(
|
||||
'format' => 'full_html',
|
||||
'name' => 'Full HTML',
|
||||
'weight' => 1,
|
||||
'filters' => array(),
|
||||
));
|
||||
$full_html_format->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the configurable text editor manager.
|
||||
*/
|
||||
public function testManager() {
|
||||
$this->editorManager = $this->container->get('plugin.manager.editor');
|
||||
|
||||
// Case 1: no text editor available:
|
||||
// - listOptions() should return an empty list of options
|
||||
// - getAttachments() should return an empty #attachments array (and not
|
||||
// a JS settings structure that is empty)
|
||||
$this->assertIdentical(array(), $this->editorManager->listOptions(), 'When no text editor is enabled, the manager works correctly.');
|
||||
$this->assertIdentical(array(), $this->editorManager->getAttachments(array()), 'No attachments when no text editor is enabled and retrieving attachments for zero text formats.');
|
||||
$this->assertIdentical(array(), $this->editorManager->getAttachments(array('filtered_html', 'full_html')), 'No attachments when no text editor is enabled and retrieving attachments for multiple text formats.');
|
||||
|
||||
// Enable the Text Editor Test module, which has the Unicorn Editor and
|
||||
// clear the editor manager's cache so it is picked up.
|
||||
$this->enableModules(array('editor_test'));
|
||||
$this->editorManager = $this->container->get('plugin.manager.editor');
|
||||
$this->editorManager->clearCachedDefinitions();
|
||||
|
||||
// Case 2: a text editor available.
|
||||
$this->assertIdentical('Unicorn Editor', (string) $this->editorManager->listOptions()['unicorn'], 'When some text editor is enabled, the manager works correctly.');
|
||||
|
||||
// Case 3: a text editor available & associated (but associated only with
|
||||
// the 'Full HTML' text format).
|
||||
$unicorn_plugin = $this->editorManager->createInstance('unicorn');
|
||||
$editor = entity_create('editor', array(
|
||||
'format' => 'full_html',
|
||||
'editor' => 'unicorn',
|
||||
));
|
||||
$editor->save();
|
||||
$this->assertIdentical(array(), $this->editorManager->getAttachments(array()), 'No attachments when one text editor is enabled and retrieving attachments for zero text formats.');
|
||||
$expected = array(
|
||||
'library' => array(
|
||||
0 => 'editor_test/unicorn',
|
||||
),
|
||||
'drupalSettings' => [
|
||||
'editor' => [
|
||||
'formats' => [
|
||||
'full_html' => [
|
||||
'format' => 'full_html',
|
||||
'editor' => 'unicorn',
|
||||
'editorSettings' => $unicorn_plugin->getJSSettings($editor),
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
);
|
||||
$this->assertIdentical($expected, $this->editorManager->getAttachments(array('filtered_html', 'full_html')), 'Correct attachments when one text editor is enabled and retrieving attachments for multiple text formats.');
|
||||
|
||||
// Case 4: a text editor available associated, but now with its JS settings
|
||||
// being altered via hook_editor_js_settings_alter().
|
||||
\Drupal::state()->set('editor_test_js_settings_alter_enabled', TRUE);
|
||||
$expected['drupalSettings']['editor']['formats']['full_html']['editorSettings']['ponyModeEnabled'] = FALSE;
|
||||
$this->assertIdentical($expected, $this->editorManager->getAttachments(array('filtered_html', 'full_html')), 'hook_editor_js_settings_alter() works correctly.');
|
||||
}
|
||||
|
||||
}
|
439
core/modules/editor/src/Tests/EditorSecurityTest.php
Normal file
439
core/modules/editor/src/Tests/EditorSecurityTest.php
Normal file
|
@ -0,0 +1,439 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Tests\EditorSecurityTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\Component\Utility\SafeMarkup;
|
||||
|
||||
/**
|
||||
* Tests XSS protection for content creators when using text editors.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorSecurityTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* The sample content to use in all tests.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $sampleContent = '<p style="color: red">Hello, Dumbo Octopus!</p><script>alert(0)</script><embed type="image/svg+xml" src="image.svg" />';
|
||||
|
||||
/**
|
||||
* The secured sample content to use in most tests.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $sampleContentSecured = '<p>Hello, Dumbo Octopus!</p>alert(0)';
|
||||
|
||||
/**
|
||||
* The secured sample content to use in tests when the <embed> tag is allowed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $sampleContentSecuredEmbedAllowed = '<p>Hello, Dumbo Octopus!</p>alert(0)<embed type="image/svg+xml" src="image.svg" />';
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('filter', 'editor', 'editor_test', 'node');
|
||||
|
||||
/**
|
||||
* User with access to Restricted HTML text format without text editor.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $untrustedUser;
|
||||
|
||||
/**
|
||||
* User with access to Restricted HTML text format with text editor.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $normalUser;
|
||||
|
||||
/**
|
||||
* User with access to Restricted HTML text format, dangerous tags allowed
|
||||
* with text editor.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $trustedUser;
|
||||
|
||||
/**
|
||||
* User with access to all text formats and text editors.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $privilegedUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create 5 text formats, to cover all potential use cases:
|
||||
// 1. restricted_without_editor (untrusted: anonymous)
|
||||
// 2. restricted_with_editor (normal: authenticated)
|
||||
// 3. restricted_plus_dangerous_tag_with_editor (privileged: trusted)
|
||||
// 4. unrestricted_without_editor (privileged: admin)
|
||||
// 5. unrestricted_with_editor (privileged: admin)
|
||||
// With text formats 2, 3 and 5, we also associate a text editor that does
|
||||
// not guarantee XSS safety. "restricted" means the text format has XSS
|
||||
// filters on output, "unrestricted" means the opposite.
|
||||
$format = entity_create('filter_format', array(
|
||||
'format' => 'restricted_without_editor',
|
||||
'name' => 'Restricted HTML, without text editor',
|
||||
'weight' => 0,
|
||||
'filters' => array(
|
||||
// A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
|
||||
'filter_html' => array(
|
||||
'status' => 1,
|
||||
'settings' => array(
|
||||
'allowed_html' => '<h4> <h5> <h6> <p> <br> <strong> <a>',
|
||||
)
|
||||
),
|
||||
),
|
||||
));
|
||||
$format->save();
|
||||
$format = entity_create('filter_format', array(
|
||||
'format' => 'restricted_with_editor',
|
||||
'name' => 'Restricted HTML, with text editor',
|
||||
'weight' => 1,
|
||||
'filters' => array(
|
||||
// A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
|
||||
'filter_html' => array(
|
||||
'status' => 1,
|
||||
'settings' => array(
|
||||
'allowed_html' => '<h4> <h5> <h6> <p> <br> <strong> <a>',
|
||||
)
|
||||
),
|
||||
),
|
||||
));
|
||||
$format->save();
|
||||
$editor = entity_create('editor', array(
|
||||
'format' => 'restricted_with_editor',
|
||||
'editor' => 'unicorn',
|
||||
));
|
||||
$editor->save();
|
||||
$format = entity_create('filter_format', array(
|
||||
'format' => 'restricted_plus_dangerous_tag_with_editor',
|
||||
'name' => 'Restricted HTML, dangerous tag allowed, with text editor',
|
||||
'weight' => 1,
|
||||
'filters' => array(
|
||||
// A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
|
||||
'filter_html' => array(
|
||||
'status' => 1,
|
||||
'settings' => array(
|
||||
'allowed_html' => '<h4> <h5> <h6> <p> <br> <strong> <a> <embed>',
|
||||
)
|
||||
),
|
||||
),
|
||||
));
|
||||
$format->save();
|
||||
$editor = entity_create('editor', array(
|
||||
'format' => 'restricted_plus_dangerous_tag_with_editor',
|
||||
'editor' => 'unicorn',
|
||||
));
|
||||
$editor->save();
|
||||
$format = entity_create('filter_format', array(
|
||||
'format' => 'unrestricted_without_editor',
|
||||
'name' => 'Unrestricted HTML, without text editor',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$format->save();
|
||||
$format = entity_create('filter_format', array(
|
||||
'format' => 'unrestricted_with_editor',
|
||||
'name' => 'Unrestricted HTML, with text editor',
|
||||
'weight' => 1,
|
||||
'filters' => array(),
|
||||
));
|
||||
$format->save();
|
||||
$editor = entity_create('editor', array(
|
||||
'format' => 'unrestricted_with_editor',
|
||||
'editor' => 'unicorn',
|
||||
));
|
||||
$editor->save();
|
||||
|
||||
|
||||
// Create node type.
|
||||
$this->drupalCreateContentType(array(
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
));
|
||||
|
||||
// Create 4 users, each with access to different text formats/editors:
|
||||
// - "untrusted": restricted_without_editor
|
||||
// - "normal": restricted_with_editor,
|
||||
// - "trusted": restricted_plus_dangerous_tag_with_editor
|
||||
// - "privileged": restricted_without_editor, restricted_with_editor,
|
||||
// restricted_plus_dangerous_tag_with_editor,
|
||||
// unrestricted_without_editor and unrestricted_with_editor
|
||||
$this->untrustedUser = $this->drupalCreateUser(array(
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'use text format restricted_without_editor',
|
||||
));
|
||||
$this->normalUser = $this->drupalCreateUser(array(
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'use text format restricted_with_editor',
|
||||
));
|
||||
$this->trustedUser = $this->drupalCreateUser(array(
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'use text format restricted_plus_dangerous_tag_with_editor',
|
||||
));
|
||||
$this->privilegedUser = $this->drupalCreateUser(array(
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'use text format restricted_without_editor',
|
||||
'use text format restricted_with_editor',
|
||||
'use text format restricted_plus_dangerous_tag_with_editor',
|
||||
'use text format unrestricted_without_editor',
|
||||
'use text format unrestricted_with_editor',
|
||||
));
|
||||
|
||||
// Create an "article" node for each possible text format, with the same
|
||||
// sample content, to do our tests on.
|
||||
$samples = array(
|
||||
array('author' => $this->untrustedUser->id(), 'format' => 'restricted_without_editor'),
|
||||
array('author' => $this->normalUser->id(), 'format' => 'restricted_with_editor'),
|
||||
array('author' => $this->trustedUser->id(), 'format' => 'restricted_plus_dangerous_tag_with_editor'),
|
||||
array('author' => $this->privilegedUser->id(), 'format' => 'unrestricted_without_editor'),
|
||||
array('author' => $this->privilegedUser->id(), 'format' => 'unrestricted_with_editor'),
|
||||
);
|
||||
foreach ($samples as $sample) {
|
||||
$this->drupalCreateNode(array(
|
||||
'type' => 'article',
|
||||
'body' => array(
|
||||
array('value' => self::$sampleContent, 'format' => $sample['format'])
|
||||
),
|
||||
'uid' => $sample['author']
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests initial security: is the user safe without switching text formats?
|
||||
*
|
||||
* Tests 8 scenarios. Tests only with a text editor that is not XSS-safe.
|
||||
*/
|
||||
function testInitialSecurity() {
|
||||
$expected = array(
|
||||
array(
|
||||
'node_id' => 1,
|
||||
'format' => 'restricted_without_editor',
|
||||
// No text editor => no XSS filtering.
|
||||
'value' => self::$sampleContent,
|
||||
'users' => array(
|
||||
$this->untrustedUser,
|
||||
$this->privilegedUser,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 2,
|
||||
'format' => 'restricted_with_editor',
|
||||
// Text editor => XSS filtering.
|
||||
'value' => self::$sampleContentSecured,
|
||||
'users' => array(
|
||||
$this->normalUser,
|
||||
$this->privilegedUser,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 3,
|
||||
'format' => 'restricted_plus_dangerous_tag_with_editor',
|
||||
// Text editor => XSS filtering.
|
||||
'value' => self::$sampleContentSecuredEmbedAllowed,
|
||||
'users' => array(
|
||||
$this->trustedUser,
|
||||
$this->privilegedUser,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 4,
|
||||
'format' => 'unrestricted_without_editor',
|
||||
// No text editor => no XSS filtering.
|
||||
'value' => self::$sampleContent,
|
||||
'users' => array(
|
||||
$this->privilegedUser,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 5,
|
||||
'format' => 'unrestricted_with_editor',
|
||||
// Text editor, no security filter => no XSS filtering.
|
||||
'value' => self::$sampleContent,
|
||||
'users' => array(
|
||||
$this->privilegedUser,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Log in as each user that may edit the content, and assert the value.
|
||||
foreach ($expected as $case) {
|
||||
foreach ($case['users'] as $account) {
|
||||
$this->pass(format_string('Scenario: sample %sample_id, %format.', array(
|
||||
'%sample_id' => $case['node_id'],
|
||||
'%format' => $case['format'],
|
||||
)));
|
||||
$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.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests administrator security: is the user safe when switching text formats?
|
||||
*
|
||||
* Tests 24 scenarios. Tests only with a text editor that is not XSS-safe.
|
||||
*
|
||||
* When changing from a more restrictive text format with a text editor (or a
|
||||
* text format without a text editor) to a less restrictive text format, it is
|
||||
* possible that a malicious user could trigger an XSS.
|
||||
*
|
||||
* E.g. when switching a piece of text that uses the Restricted HTML text
|
||||
* format and contains a <script> tag to the Full HTML text format, the
|
||||
* <script> tag would be executed. Unless we apply appropriate filtering.
|
||||
*/
|
||||
function testSwitchingSecurity() {
|
||||
$expected = array(
|
||||
array(
|
||||
'node_id' => 1,
|
||||
'value' => self::$sampleContent, // No text editor => no XSS filtering.
|
||||
'format' => 'restricted_without_editor',
|
||||
'switch_to' => array(
|
||||
'restricted_with_editor' => self::$sampleContentSecured,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
|
||||
// No text editor => no XSS filtering.
|
||||
'unrestricted_without_editor' => FALSE,
|
||||
'unrestricted_with_editor' => self::$sampleContentSecured,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 2,
|
||||
'value' => self::$sampleContentSecured, // Text editor => XSS filtering.
|
||||
'format' => 'restricted_with_editor',
|
||||
'switch_to' => array(
|
||||
// No text editor => no XSS filtering.
|
||||
'restricted_without_editor' => FALSE,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
|
||||
// No text editor => no XSS filtering.
|
||||
'unrestricted_without_editor' => FALSE,
|
||||
'unrestricted_with_editor' => self::$sampleContentSecured,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 3,
|
||||
'value' => self::$sampleContentSecuredEmbedAllowed, // Text editor => XSS filtering.
|
||||
'format' => 'restricted_plus_dangerous_tag_with_editor',
|
||||
'switch_to' => array(
|
||||
// No text editor => no XSS filtering.
|
||||
'restricted_without_editor' => FALSE,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'restricted_with_editor' => self::$sampleContentSecured,
|
||||
// No text editor => no XSS filtering.
|
||||
'unrestricted_without_editor' => FALSE,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'unrestricted_with_editor' => self::$sampleContentSecured,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 4,
|
||||
'value' => self::$sampleContent, // No text editor => no XSS filtering.
|
||||
'format' => 'unrestricted_without_editor',
|
||||
'switch_to' => array(
|
||||
// No text editor => no XSS filtering.
|
||||
'restricted_without_editor' => FALSE,
|
||||
'restricted_with_editor' => self::$sampleContentSecured,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
|
||||
// From no editor, no security filters, to editor, still no security
|
||||
// filters: resulting content when viewed was already vulnerable, so
|
||||
// it must be intentional.
|
||||
'unrestricted_with_editor' => FALSE,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 5,
|
||||
'value' => self::$sampleContentSecured, // Text editor => XSS filtering.
|
||||
'format' => 'unrestricted_with_editor',
|
||||
'switch_to' => array(
|
||||
// From editor, no security filters to security filters, no editor: no
|
||||
// risk.
|
||||
'restricted_without_editor' => FALSE,
|
||||
'restricted_with_editor' => self::$sampleContentSecured,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
|
||||
// From no editor, no security filters, to editor, still no security
|
||||
// filters: resulting content when viewed was already vulnerable, so
|
||||
// it must be intentional.
|
||||
'unrestricted_without_editor' => FALSE,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Log in as the privileged user, and for every sample, do the following:
|
||||
// - switch to every other text format/editor
|
||||
// - assert the XSS-filtered values that we get from the server
|
||||
$value_original_attribute = SafeMarkup::checkPlain(self::$sampleContent);
|
||||
$this->drupalLogin($this->privilegedUser);
|
||||
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.');
|
||||
|
||||
// Switch to every other text format/editor and verify the results.
|
||||
foreach ($case['switch_to'] as $format => $expected_filtered_value) {
|
||||
$this->pass(format_string('Scenario: sample %sample_id, switch from %original_format to %format.', array(
|
||||
'%sample_id' => $case['node_id'],
|
||||
'%original_format' => $case['format'],
|
||||
'%format' => $format,
|
||||
)));
|
||||
$post = array(
|
||||
'value' => self::$sampleContent,
|
||||
'original_format_id' => $case['format'],
|
||||
);
|
||||
$response = $this->drupalPostWithFormat('editor/filter_xss/' . $format, 'json', $post);
|
||||
$this->assertResponse(200);
|
||||
$json = Json::decode($response);
|
||||
$this->assertIdentical($json, $expected_filtered_value, 'The value was correctly filtered for XSS attack vectors.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the standard text editor XSS filter being overridden.
|
||||
*/
|
||||
function testEditorXssFilterOverride() {
|
||||
// First: the Standard text editor XSS filter.
|
||||
$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.');
|
||||
|
||||
// Enable editor_test.module's hook_editor_xss_filter_alter() implementation
|
||||
// to alter the text editor XSS filter class being used.
|
||||
\Drupal::state()->set('editor_test_editor_xss_filter_alter_enabled', TRUE);
|
||||
|
||||
// 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.');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Tests\QuickEditIntegrationLoadingTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* Tests Quick Edit module integration endpoints.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class QuickEditIntegrationLoadingTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('quickedit', 'filter', 'node', 'editor');
|
||||
|
||||
/**
|
||||
* The basic permissions necessary to view content and use in-place editing.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $basicPermissions = array('access content', 'create article content', 'use text format filtered_html', 'access contextual links');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a text format.
|
||||
$filtered_html_format = entity_create('filter_format', array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(
|
||||
'filter_caption' => array(
|
||||
'status' => 1,
|
||||
),
|
||||
),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
|
||||
// Create a node type.
|
||||
$this->drupalCreateContentType(array(
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
));
|
||||
|
||||
// Create one node of the above node type using the above text format.
|
||||
$this->drupalCreateNode(array(
|
||||
'type' => 'article',
|
||||
'body' => array(
|
||||
0 => array(
|
||||
'value' => '<p>Do you also love Drupal?</p><img src="druplicon.png" data-caption="Druplicon" />',
|
||||
'format' => 'filtered_html',
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test loading of untransformed text when a user doesn't have access to it.
|
||||
*/
|
||||
public function testUsersWithoutPermission() {
|
||||
// Create 3 users, each with insufficient permissions, i.e. without either
|
||||
// or both of the following permissions:
|
||||
// - the 'access in-place editing' permission
|
||||
// - the 'edit any article content' permission (necessary to edit node 1)
|
||||
$users = array(
|
||||
$this->drupalCreateUser(static::$basicPermissions),
|
||||
$this->drupalCreateUser(array_merge(static::$basicPermissions, array('edit any article content'))),
|
||||
$this->drupalCreateUser(array_merge(static::$basicPermissions, array('access in-place editing')))
|
||||
);
|
||||
|
||||
// Now test with each of the 3 users with insufficient permissions.
|
||||
foreach ($users as $user) {
|
||||
$this->drupalLogin($user);
|
||||
$this->drupalGet('node/1');
|
||||
|
||||
// Ensure the text is transformed.
|
||||
$this->assertRaw('<p>Do you also love Drupal?</p><figure class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
|
||||
|
||||
// Retrieving the untransformed text should result in an empty 403 response.
|
||||
$response = $this->drupalPost('editor/' . 'node/1/body/en/full', '', array(), array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax')));
|
||||
$this->assertResponse(403);
|
||||
$this->assertIdentical('{}', $response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test loading of untransformed text when a user does have access to it.
|
||||
*/
|
||||
public function testUserWithPermission() {
|
||||
$user = $this->drupalCreateUser(array_merge(static::$basicPermissions, array('edit any article content', 'access in-place editing')));
|
||||
$this->drupalLogin($user);
|
||||
$this->drupalGet('node/1');
|
||||
|
||||
// Ensure the text is transformed.
|
||||
$this->assertRaw('<p>Do you also love Drupal?</p><figure class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
|
||||
|
||||
$response = $this->drupalPost('editor/' . 'node/1/body/en/full', 'application/vnd.drupal-ajax', array());
|
||||
$this->assertResponse(200);
|
||||
$ajax_commands = Json::decode($response);
|
||||
$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.');
|
||||
}
|
||||
|
||||
}
|
234
core/modules/editor/src/Tests/QuickEditIntegrationTest.php
Normal file
234
core/modules/editor/src/Tests/QuickEditIntegrationTest.php
Normal file
|
@ -0,0 +1,234 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor\Tests\QuickEditIntegrationTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\quickedit\EditorSelector;
|
||||
use Drupal\quickedit\MetadataGenerator;
|
||||
use Drupal\quickedit\Plugin\InPlaceEditorManager;
|
||||
use Drupal\quickedit\Tests\QuickEditTestBase;
|
||||
use Drupal\quickedit_test\MockEditEntityFieldAccessCheck;
|
||||
use Drupal\editor\EditorController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
|
||||
use Symfony\Component\HttpKernel\HttpKernelInterface;
|
||||
|
||||
/**
|
||||
* Tests Edit module integration (Editor module's inline editing support).
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class QuickEditIntegrationTest extends QuickEditTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static $modules = array('editor', 'editor_test');
|
||||
|
||||
/**
|
||||
* The manager for editor plug-ins.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected $editorManager;
|
||||
|
||||
/**
|
||||
* The metadata generator object to be tested.
|
||||
*
|
||||
* @var \Drupal\quickedit\MetadataGeneratorInterface.php
|
||||
*/
|
||||
protected $metadataGenerator;
|
||||
|
||||
/**
|
||||
* The editor selector object to be used by the metadata generator object.
|
||||
*
|
||||
* @var \Drupal\quickedit\EditorSelectorInterface
|
||||
*/
|
||||
protected $editorSelector;
|
||||
|
||||
/**
|
||||
* The access checker object to be used by the metadata generator object.
|
||||
*
|
||||
* @var \Drupal\quickedit\Access\EditEntityFieldAccessCheckInterface
|
||||
*/
|
||||
protected $accessChecker;
|
||||
|
||||
/**
|
||||
* The name of the field ued for tests.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $fieldName;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Install the Filter module.
|
||||
$this->installSchema('system', 'url_alias');
|
||||
|
||||
// Create a field.
|
||||
$this->fieldName = 'field_textarea';
|
||||
$this->createFieldWithStorage(
|
||||
$this->fieldName, 'text', 1, 'Long text field',
|
||||
// Instance settings.
|
||||
array(),
|
||||
// Widget type & settings.
|
||||
'text_textarea',
|
||||
array('size' => 42),
|
||||
// 'default' formatter type & settings.
|
||||
'text_default',
|
||||
array()
|
||||
);
|
||||
|
||||
// Create text format.
|
||||
$full_html_format = entity_create('filter_format', array(
|
||||
'format' => 'full_html',
|
||||
'name' => 'Full HTML',
|
||||
'weight' => 1,
|
||||
'filters' => array(),
|
||||
));
|
||||
$full_html_format->save();
|
||||
|
||||
// Associate text editor with text format.
|
||||
$editor = entity_create('editor', array(
|
||||
'format' => $full_html_format->id(),
|
||||
'editor' => 'unicorn',
|
||||
));
|
||||
$editor->save();
|
||||
|
||||
// Also create a text format without an associated text editor.
|
||||
entity_create('filter_format', array(
|
||||
'format' => 'no_editor',
|
||||
'name' => 'No Text Editor',
|
||||
'weight' => 2,
|
||||
'filters' => array(),
|
||||
))->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the in-place editor that Edit selects.
|
||||
*/
|
||||
protected function getSelectedEditor($entity_id, $field_name, $view_mode = 'default') {
|
||||
$entity = entity_load('entity_test', $entity_id, TRUE);
|
||||
$items = $entity->getTranslation(LanguageInterface::LANGCODE_NOT_SPECIFIED)->get($field_name);
|
||||
$options = entity_get_display('entity_test', 'entity_test', $view_mode)->getComponent($field_name);
|
||||
return $this->editorSelector->getEditor($options['type'], $items);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests editor selection when the Editor module is present.
|
||||
*
|
||||
* Tests a textual field, with text filtering, with cardinality 1 and >1,
|
||||
* always with a ProcessedTextEditor plug-in present, but with varying text
|
||||
* format compatibility.
|
||||
*/
|
||||
public function testEditorSelection() {
|
||||
$this->editorManager = $this->container->get('plugin.manager.quickedit.editor');
|
||||
$this->editorSelector = $this->container->get('quickedit.editor.selector');
|
||||
|
||||
// Create an entity with values for this text field.
|
||||
$entity = entity_create('entity_test');
|
||||
$entity->{$this->fieldName}->value = 'Hello, world!';
|
||||
$entity->{$this->fieldName}->format = 'filtered_html';
|
||||
$entity->save();
|
||||
|
||||
// Editor selection w/ cardinality 1, text format w/o associated text editor.
|
||||
$this->assertEqual('form', $this->getSelectedEditor($entity->id(), $this->fieldName), "With cardinality 1, and the filtered_html text format, the 'form' editor is selected.");
|
||||
|
||||
// Editor selection w/ cardinality 1, text format w/ associated text editor.
|
||||
$entity->{$this->fieldName}->format = 'full_html';
|
||||
$entity->save();
|
||||
$this->assertEqual('editor', $this->getSelectedEditor($entity->id(), $this->fieldName), "With cardinality 1, and the full_html text format, the 'editor' editor is selected.");
|
||||
|
||||
// Editor selection with text processing, cardinality >1
|
||||
$this->fields->field_textarea_field_storage->setCardinality(2);
|
||||
$this->fields->field_textarea_field_storage->save();
|
||||
$this->assertEqual('form', $this->getSelectedEditor($entity->id(), $this->fieldName), "With cardinality >1, and both items using the full_html text format, the 'form' editor is selected.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests (custom) metadata when the formatted text editor is used.
|
||||
*/
|
||||
public function testMetadata() {
|
||||
$this->editorManager = $this->container->get('plugin.manager.quickedit.editor');
|
||||
$this->accessChecker = new MockEditEntityFieldAccessCheck();
|
||||
$this->editorSelector = $this->container->get('quickedit.editor.selector');
|
||||
$this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector, $this->editorManager);
|
||||
|
||||
// Create an entity with values for the field.
|
||||
$entity = entity_create('entity_test');
|
||||
$entity->{$this->fieldName}->value = 'Test';
|
||||
$entity->{$this->fieldName}->format = 'full_html';
|
||||
$entity->save();
|
||||
$entity = entity_load('entity_test', $entity->id());
|
||||
|
||||
// Verify metadata.
|
||||
$items = $entity->getTranslation(LanguageInterface::LANGCODE_NOT_SPECIFIED)->get($this->fieldName);
|
||||
$metadata = $this->metadataGenerator->generateFieldMetadata($items, 'default');
|
||||
$expected = array(
|
||||
'access' => TRUE,
|
||||
'label' => 'Long text field',
|
||||
'editor' => 'editor',
|
||||
'aria' => 'Entity entity_test 1, field Long text field',
|
||||
'custom' => array(
|
||||
'format' => 'full_html',
|
||||
'formatHasTransformations' => FALSE,
|
||||
),
|
||||
);
|
||||
$this->assertEqual($expected, $metadata, 'The correct metadata (including custom metadata) is generated.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests in-place editor attachments when the Editor module is present.
|
||||
*/
|
||||
public function testAttachments() {
|
||||
$this->editorSelector = $this->container->get('quickedit.editor.selector');
|
||||
|
||||
$editors = array('editor');
|
||||
$attachments = $this->editorSelector->getEditorAttachments($editors);
|
||||
$this->assertIdentical($attachments, array('library' => array('editor/quickedit.inPlaceEditor.formattedText')), "Expected attachments for Editor module's in-place editor found.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests GetUntransformedTextCommand AJAX command.
|
||||
*/
|
||||
public function testGetUntransformedTextCommand() {
|
||||
// Create an entity with values for the field.
|
||||
$entity = entity_create('entity_test');
|
||||
$entity->{$this->fieldName}->value = 'Test';
|
||||
$entity->{$this->fieldName}->format = 'full_html';
|
||||
$entity->save();
|
||||
$entity = entity_load('entity_test', $entity->id());
|
||||
|
||||
// Verify AJAX response.
|
||||
$controller = new EditorController();
|
||||
$request = new Request();
|
||||
$response = $controller->getUntransformedText($entity, $this->fieldName, LanguageInterface::LANGCODE_NOT_SPECIFIED, 'default');
|
||||
$expected = array(
|
||||
array(
|
||||
'command' => 'editorGetUntransformedText',
|
||||
'data' => 'Test',
|
||||
)
|
||||
);
|
||||
|
||||
$ajax_response_attachments_processor = \Drupal::service('ajax_response.attachments_processor');
|
||||
$subscriber = new AjaxResponseSubscriber($ajax_response_attachments_processor);
|
||||
$event = new FilterResponseEvent(
|
||||
\Drupal::service('http_kernel'),
|
||||
$request,
|
||||
HttpKernelInterface::MASTER_REQUEST,
|
||||
$response
|
||||
);
|
||||
$subscriber->onResponse($event);
|
||||
|
||||
$this->assertEqual(Json::encode($expected), $response->getContent(), 'The GetUntransformedTextCommand AJAX command works correctly.');
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
# Schema for the configuration files of the Editor test module.
|
||||
|
||||
editor.settings.unicorn:
|
||||
type: mapping
|
||||
label: 'Unicorn settings'
|
||||
mapping:
|
||||
ponies_too:
|
||||
type: boolean
|
||||
label: 'Ponies too'
|
||||
|
||||
editor.settings.trex:
|
||||
type: mapping
|
||||
label: 'T-Rex settings'
|
||||
mapping:
|
||||
stumpy_arms:
|
||||
type: boolean
|
||||
label: 'Stumpy arms'
|
6
core/modules/editor/tests/modules/editor_test.info.yml
Normal file
6
core/modules/editor/tests/modules/editor_test.info.yml
Normal file
|
@ -0,0 +1,6 @@
|
|||
name: 'Text Editor test'
|
||||
type: module
|
||||
description: 'Support module for the Text Editor module tests.'
|
||||
core: 8.x
|
||||
package: Testing
|
||||
version: VERSION
|
|
@ -0,0 +1,8 @@
|
|||
unicorn:
|
||||
version: VERSION
|
||||
js:
|
||||
unicorn.js: {}
|
||||
trex:
|
||||
version: VERSION
|
||||
js:
|
||||
trex.js: {}
|
46
core/modules/editor/tests/modules/editor_test.module
Normal file
46
core/modules/editor/tests/modules/editor_test.module
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Helper module for the Text Editor tests.
|
||||
*/
|
||||
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_editor_js_settings_alter().
|
||||
*/
|
||||
function editor_test_editor_js_settings_alter(&$settings) {
|
||||
// Allow tests to enable or disable this alter hook.
|
||||
if (!\Drupal::state()->get('editor_test_js_settings_alter_enabled', FALSE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isset($settings['editor']['formats']['full_html'])) {
|
||||
$settings['editor']['formats']['full_html']['editorSettings']['ponyModeEnabled'] = FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_editor_xss_filter_alter().
|
||||
*/
|
||||
function editor_test_editor_xss_filter_alter(&$editor_xss_filter_class, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL) {
|
||||
// Allow tests to enable or disable this alter hook.
|
||||
if (!\Drupal::state()->get('editor_test_editor_xss_filter_alter_enabled', FALSE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$filters = $format->filters()->getAll();
|
||||
if (isset($filters['filter_html']) && $filters['filter_html']->status) {
|
||||
$editor_xss_filter_class = '\Drupal\editor_test\EditorXssFilter\Insecure';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_editor_info_alter().
|
||||
*/
|
||||
function editor_test_editor_info_alter(&$items) {
|
||||
if (!\Drupal::state()->get('editor_test_give_me_a_trex_thanks', FALSE)) {
|
||||
unset($items['trex']);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor_test\EditorXssFilter\Insecure.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor_test\EditorXssFilter;
|
||||
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Drupal\editor\EditorXssFilterInterface;
|
||||
|
||||
/**
|
||||
* Defines an insecure text editor XSS filter (for testing purposes).
|
||||
*/
|
||||
class Insecure implements EditorXssFilterInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function filterXss($html, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL) {
|
||||
// Don't apply any XSS filtering, just return the string we received.
|
||||
return $html;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor_test\Plugin\Editor\TRexEditor.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor_test\Plugin\Editor;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\Plugin\EditorBase;
|
||||
use Drupal\editor\Entity\Editor as EditorEntity;
|
||||
|
||||
/**
|
||||
* Defines a Tyrannosaurus-Rex powered text editor for testing purposes.
|
||||
*
|
||||
* @Editor(
|
||||
* id = "trex",
|
||||
* label = @Translation("TRex Editor"),
|
||||
* supports_content_filtering = TRUE,
|
||||
* supports_inline_editing = TRUE,
|
||||
* is_xss_safe = FALSE,
|
||||
* supported_element_types = {
|
||||
* "textarea",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class TRexEditor extends EditorBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultSettings() {
|
||||
return array('stumpy_arms' => TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, EditorEntity $editor) {
|
||||
$form['stumpy_arms'] = array(
|
||||
'#title' => t('Stumpy arms'),
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => TRUE,
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getJSSettings(EditorEntity $editor) {
|
||||
$js_settings = array();
|
||||
$settings = $editor->getSettings();
|
||||
if ($settings['stumpy_arms']) {
|
||||
$js_settings['doMyArmsLookStumpy'] = TRUE;
|
||||
}
|
||||
return $js_settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibraries(EditorEntity $editor) {
|
||||
return array(
|
||||
'editor_test/trex',
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\editor_test\Plugin\Editor\UnicornEditor.
|
||||
*/
|
||||
|
||||
namespace Drupal\editor_test\Plugin\Editor;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\Plugin\EditorBase;
|
||||
use Drupal\editor\Entity\Editor as EditorEntity;
|
||||
|
||||
/**
|
||||
* Defines a Unicorn-powered text editor for Drupal (for testing purposes).
|
||||
*
|
||||
* @Editor(
|
||||
* id = "unicorn",
|
||||
* label = @Translation("Unicorn Editor"),
|
||||
* supports_content_filtering = TRUE,
|
||||
* supports_inline_editing = TRUE,
|
||||
* is_xss_safe = FALSE,
|
||||
* supported_element_types = {
|
||||
* "textarea",
|
||||
* "textfield",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class UnicornEditor extends EditorBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getDefaultSettings() {
|
||||
return array('ponies_too' => TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function settingsForm(array $form, FormStateInterface $form_state, EditorEntity $editor) {
|
||||
$form['ponies_too'] = array(
|
||||
'#title' => t('Pony mode'),
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => TRUE,
|
||||
);
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getJSSettings(EditorEntity $editor) {
|
||||
$js_settings = array();
|
||||
$settings = $editor->getSettings();
|
||||
if ($settings['ponies_too']) {
|
||||
$js_settings['ponyModeEnabled'] = TRUE;
|
||||
}
|
||||
return $js_settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibraries(EditorEntity $editor) {
|
||||
return array(
|
||||
'editor_test/unicorn',
|
||||
);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Tests\editor\Unit\EditorConfigEntityUnitTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\Tests\editor\Unit;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerBuilder;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\editor\Entity\Editor
|
||||
* @group editor
|
||||
*/
|
||||
class EditorConfigEntityUnitTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The entity type used for testing.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $entityType;
|
||||
|
||||
/**
|
||||
* The entity manager used for testing.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityManagerInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $entityManager;
|
||||
|
||||
/**
|
||||
* The ID of the type of the entity under test.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $entityTypeId;
|
||||
|
||||
/**
|
||||
* The UUID generator used for testing.
|
||||
*
|
||||
* @var \Drupal\Component\Uuid\UuidInterface|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $uuid;
|
||||
|
||||
/**
|
||||
* The editor plugin manager used for testing.
|
||||
*
|
||||
* @var \Drupal\editor\Plugin\EditorManager|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $editorPluginManager;
|
||||
|
||||
/**
|
||||
* Editor plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $editorId;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
$this->editorId = $this->randomMachineName();
|
||||
$this->entityTypeId = $this->randomMachineName();
|
||||
|
||||
$this->entityType = $this->getMock('\Drupal\Core\Entity\EntityTypeInterface');
|
||||
$this->entityType->expects($this->any())
|
||||
->method('getProvider')
|
||||
->will($this->returnValue('editor'));
|
||||
|
||||
$this->entityManager = $this->getMock('\Drupal\Core\Entity\EntityManagerInterface');
|
||||
$this->entityManager->expects($this->any())
|
||||
->method('getDefinition')
|
||||
->with($this->entityTypeId)
|
||||
->will($this->returnValue($this->entityType));
|
||||
|
||||
$this->uuid = $this->getMock('\Drupal\Component\Uuid\UuidInterface');
|
||||
|
||||
$this->editorPluginManager = $this->getMockBuilder('Drupal\editor\Plugin\EditorManager')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
|
||||
$container = new ContainerBuilder();
|
||||
$container->set('entity.manager', $this->entityManager);
|
||||
$container->set('uuid', $this->uuid);
|
||||
$container->set('plugin.manager.editor', $this->editorPluginManager);
|
||||
\Drupal::setContainer($container);
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::calculateDependencies
|
||||
*/
|
||||
public function testCalculateDependencies() {
|
||||
$format_id = 'filter.format.test';
|
||||
$values = array('editor' => $this->editorId, 'format' => $format_id);
|
||||
|
||||
$plugin = $this->getMockBuilder('Drupal\editor\Plugin\EditorPluginInterface')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$plugin->expects($this->once())
|
||||
->method('getPluginDefinition')
|
||||
->will($this->returnValue(array('provider' => 'test_module')));
|
||||
$plugin->expects($this->once())
|
||||
->method('getDefaultSettings')
|
||||
->will($this->returnValue(array()));
|
||||
|
||||
$this->editorPluginManager->expects($this->any())
|
||||
->method('createInstance')
|
||||
->with($this->editorId)
|
||||
->will($this->returnValue($plugin));
|
||||
|
||||
$entity = new Editor($values, $this->entityTypeId);
|
||||
|
||||
$filter_format = $this->getMock('Drupal\Core\Config\Entity\ConfigEntityInterface');
|
||||
$filter_format->expects($this->once())
|
||||
->method('getConfigDependencyName')
|
||||
->will($this->returnValue('filter.format.test'));
|
||||
|
||||
$storage = $this->getMock('Drupal\Core\Entity\EntityStorageInterface');
|
||||
$storage->expects($this->once())
|
||||
->method('load')
|
||||
->with($format_id)
|
||||
->will($this->returnValue($filter_format));
|
||||
|
||||
$this->entityManager->expects($this->once())
|
||||
->method('getStorage')
|
||||
->with('filter_format')
|
||||
->will($this->returnValue($storage));
|
||||
|
||||
$dependencies = $entity->calculateDependencies();
|
||||
$this->assertContains('test_module', $dependencies['module']);
|
||||
$this->assertContains('filter.format.test', $dependencies['config']);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,604 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Contains \Drupal\Tests\editor\Unit\EditorXssFilter\StandardTest.
|
||||
*/
|
||||
|
||||
namespace Drupal\Tests\editor\Unit\EditorXssFilter;
|
||||
|
||||
use Drupal\editor\EditorXssFilter\Standard;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Drupal\filter\Plugin\FilterInterface;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\editor\EditorXssFilter\Standard
|
||||
* @group editor
|
||||
*/
|
||||
class StandardTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* The mocked text format configuration entity.
|
||||
*
|
||||
* @var \Drupal\filter\Entity\FilterFormat|\PHPUnit_Framework_MockObject_MockObject
|
||||
*/
|
||||
protected $format;
|
||||
|
||||
protected function setUp() {
|
||||
|
||||
// Mock text format configuration entity object.
|
||||
$this->format = $this->getMockBuilder('\Drupal\filter\Entity\FilterFormat')
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$this->format->expects($this->any())
|
||||
->method('getFilterTypes')
|
||||
->will($this->returnValue(array(FilterInterface::TYPE_HTML_RESTRICTOR)));
|
||||
$restrictions = array(
|
||||
'allowed' => array(
|
||||
'p' => TRUE,
|
||||
'a' => TRUE,
|
||||
'*' => array(
|
||||
'style' => FALSE,
|
||||
'on*' => FALSE,
|
||||
),
|
||||
),
|
||||
);
|
||||
$this->format->expects($this->any())
|
||||
->method('getHtmlRestrictions')
|
||||
->will($this->returnValue($restrictions));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides test data for testFilterXss().
|
||||
*
|
||||
* @see \Drupal\Tests\editor\Unit\editor\EditorXssFilter\StandardTest::testFilterXss()
|
||||
*/
|
||||
public function providerTestFilterXss() {
|
||||
$data = array();
|
||||
$data[] = array('<p>Hello, world!</p><unknown>Pink Fairy Armadillo</unknown>', '<p>Hello, world!</p><unknown>Pink Fairy Armadillo</unknown>');
|
||||
$data[] = array('<p style="color:red">Hello, world!</p><unknown>Pink Fairy Armadillo</unknown>', '<p>Hello, world!</p><unknown>Pink Fairy Armadillo</unknown>');
|
||||
$data[] = array('<p>Hello, world!</p><unknown>Pink Fairy Armadillo</unknown><script>alert("evil");</script>', '<p>Hello, world!</p><unknown>Pink Fairy Armadillo</unknown>alert("evil");');
|
||||
$data[] = array('<p>Hello, world!</p><unknown>Pink Fairy Armadillo</unknown><a href="javascript:alert(1)">test</a>', '<p>Hello, world!</p><unknown>Pink Fairy Armadillo</unknown><a href="alert(1)">test</a>');
|
||||
|
||||
// All cases listed on https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet
|
||||
|
||||
// No Filter Evasion.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#No_Filter_Evasion
|
||||
$data[] = array('<SCRIPT SRC=http://ha.ckers.org/xss.js></SCRIPT>', '');
|
||||
|
||||
// Image XSS using the JavaScript directive.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Image_XSS_using_the_JavaScript_directive
|
||||
$data[] = array('<IMG SRC="javascript:alert(\'XSS\');">', '<IMG src="alert('XSS');">');
|
||||
|
||||
// No quotes and no semicolon.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#No_quotes_and_no_semicolon
|
||||
$data[] = array('<IMG SRC=javascript:alert(\'XSS\')>', '<IMG>');
|
||||
|
||||
// Case insensitive XSS attack vector.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Case_insensitive_XSS_attack_vector
|
||||
$data[] = array('<IMG SRC=JaVaScRiPt:alert(\'XSS\')>', '<IMG>');
|
||||
|
||||
// HTML entities.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#HTML_entities
|
||||
$data[] = array('<IMG SRC=javascript:alert("XSS")>', '<IMG>');
|
||||
|
||||
// Grave accent obfuscation.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Grave_accent_obfuscation
|
||||
$data[] = array('<IMG SRC=`javascript:alert("RSnake says, \'XSS\'")`>', '<IMG>');
|
||||
|
||||
// Malformed A tags.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Malformed_A_tags
|
||||
$data[] = array('<a onmouseover="alert(document.cookie)">xxs link</a>', '<a>xxs link</a>');
|
||||
$data[] = array('<a onmouseover=alert(document.cookie)>xxs link</a>', '<a>xxs link</a>');
|
||||
|
||||
// Malformed IMG tags.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Malformed_IMG_tags
|
||||
$data[] = array('<IMG """><SCRIPT>alert("XSS")</SCRIPT>">', '<IMG>alert("XSS")">');
|
||||
|
||||
// fromCharCode.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#fromCharCode
|
||||
$data[] = array('<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>', '<IMG src="alert(String.fromCharCode(88,83,83))">');
|
||||
|
||||
// Default SRC tag to get past filters that check SRC domain.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Default_SRC_tag_to_get_past_filters_that_check_SRC_domain
|
||||
$data[] = array('<IMG SRC=# onmouseover="alert(\'xxs\')">', '<IMG src="#">');
|
||||
|
||||
// Default SRC tag by leaving it empty.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Default_SRC_tag_by_leaving_it_empty
|
||||
$data[] = array('<IMG SRC= onmouseover="alert(\'xxs\')">', '<IMG nmouseover="alert('xxs')">');
|
||||
|
||||
// Default SRC tag by leaving it out entirely.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Default_SRC_tag_by_leaving_it_out_entirely
|
||||
$data[] = array('<IMG onmouseover="alert(\'xxs\')">', '<IMG>');
|
||||
|
||||
// Decimal HTML character references.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Decimal_HTML_character_references
|
||||
$data[] = array('<IMG SRC=javascript:alert('XSS')>', '<IMG src="alert('XSS')">');
|
||||
|
||||
// Decimal HTML character references without trailing semicolons.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Decimal_HTML_character_references_without_trailing_semicolons
|
||||
$data[] = array('<IMG SRC=javascript:alert('XSS')>', '<IMG src="&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041">');
|
||||
|
||||
// Hexadecimal HTML character references without trailing semicolons.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Hexadecimal_HTML_character_references_without_trailing_semicolons
|
||||
$data[] = array('<IMG SRC=javascript:alert('XSS')>', '<IMG src="&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29">');
|
||||
|
||||
// Embedded tab.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Embedded_tab
|
||||
$data[] = array('<IMG SRC="jav ascript:alert(\'XSS\');">', '<IMG src="alert('XSS');">');
|
||||
|
||||
// Embedded Encoded tab.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Embedded_Encoded_tab
|
||||
$data[] = array('<IMG SRC="jav	ascript:alert(\'XSS\');">', '<IMG src="alert('XSS');">');
|
||||
|
||||
// Embedded newline to break up XSS.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Embedded_newline_to_break_up_XSS
|
||||
$data[] = array('<IMG SRC="jav
ascript:alert(\'XSS\');">', '<IMG src="alert('XSS');">');
|
||||
|
||||
// Embedded carriage return to break up XSS.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Embedded_carriage_return_to_break_up_XSS
|
||||
$data[] = array('<IMG SRC="jav
ascript:alert(\'XSS\');">', '<IMG src="alert('XSS');">');
|
||||
|
||||
// Null breaks up JavaScript directive.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Null_breaks_up_JavaScript_directive
|
||||
$data[] = array("<IMG SRC=java\0script:alert(\"XSS\")>", '<IMG>');
|
||||
|
||||
// Spaces and meta chars before the JavaScript in images for XSS.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Spaces_and_meta_chars_before_the_JavaScript_in_images_for_XSS
|
||||
// @fixme This dataset currently fails under 5.4 because of
|
||||
// https://www.drupal.org/node/1210798. Restore after it's fixed.
|
||||
if (version_compare(PHP_VERSION, '5.4.0', '<')) {
|
||||
$data[] = array('<IMG SRC="  javascript:alert(\'XSS\');">', '<IMG src="alert('XSS');">');
|
||||
}
|
||||
|
||||
// Non-alpha-non-digit XSS.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Non-alpha-non-digit_XSS
|
||||
$data[] = array('<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>', '');
|
||||
$data[] = array('<BODY onload!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")>', '<BODY>');
|
||||
$data[] = array('<SCRIPT/SRC="http://ha.ckers.org/xss.js"></SCRIPT>', '');
|
||||
|
||||
// Extraneous open brackets.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Extraneous_open_brackets
|
||||
$data[] = array('<<SCRIPT>alert("XSS");//<</SCRIPT>', '<alert("XSS");//<');
|
||||
|
||||
// No closing script tags.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#No_closing_script_tags
|
||||
$data[] = array('<SCRIPT SRC=http://ha.ckers.org/xss.js?< B >', '');
|
||||
|
||||
// Protocol resolution in script tags.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Protocol_resolution_in_script_tags
|
||||
$data[] = array('<SCRIPT SRC=//ha.ckers.org/.j>', '');
|
||||
|
||||
// Half open HTML/JavaScript XSS vector.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Half_open_HTML.2FJavaScript_XSS_vector
|
||||
$data[] = array('<IMG SRC="javascript:alert(\'XSS\')"', '<IMG src="alert('XSS')">');
|
||||
|
||||
// Double open angle brackets.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Double_open_angle_brackets
|
||||
// @see http://ha.ckers.org/blog/20060611/hotbot-xss-vulnerability/ to
|
||||
// understand why this is a vulnerability.
|
||||
$data[] = array('<iframe src=http://ha.ckers.org/scriptlet.html <', '<iframe src="http://ha.ckers.org/scriptlet.html">');
|
||||
|
||||
// Escaping JavaScript escapes.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Escaping_JavaScript_escapes
|
||||
// This one is irrelevant for Drupal; we *never* output any JavaScript code
|
||||
// that depends on the URL's query string.
|
||||
|
||||
// End title tag.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#End_title_tag
|
||||
$data[] = array('</TITLE><SCRIPT>alert("XSS");</SCRIPT>', '</TITLE>alert("XSS");');
|
||||
|
||||
// INPUT image.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#INPUT_image
|
||||
$data[] = array('<INPUT TYPE="IMAGE" SRC="javascript:alert(\'XSS\');">', '<INPUT type="IMAGE" src="alert('XSS');">');
|
||||
|
||||
// BODY image.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#BODY_image
|
||||
$data[] = array('<BODY BACKGROUND="javascript:alert(\'XSS\')">', '<BODY background="alert('XSS')">');
|
||||
|
||||
// IMG Dynsrc.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#IMG_Dynsrc
|
||||
$data[] = array('<IMG DYNSRC="javascript:alert(\'XSS\')">', '<IMG dynsrc="alert('XSS')">');
|
||||
|
||||
// IMG lowsrc.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#IMG_lowsrc
|
||||
$data[] = array('<IMG LOWSRC="javascript:alert(\'XSS\')">', '<IMG lowsrc="alert('XSS')">');
|
||||
|
||||
// List-style-image.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#List-style-image
|
||||
$data[] = array('<STYLE>li {list-style-image: url("javascript:alert(\'XSS\')");}</STYLE><UL><LI>XSS</br>', 'li {list-style-image: url("javascript:alert(\'XSS\')");}<UL><LI>XSS</br>');
|
||||
|
||||
// VBscript in an image.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#VBscript_in_an_image
|
||||
$data[] = array('<IMG SRC=\'vbscript:msgbox("XSS")\'>', '<IMG src=\'msgbox("XSS")\'>');
|
||||
|
||||
// Livescript (older versions of Netscape only).
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Livescript_.28older_versions_of_Netscape_only.29
|
||||
$data[] = array('<IMG SRC="livescript:[code]">', '<IMG src="[code]">');
|
||||
|
||||
// BODY tag.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#BODY_tag
|
||||
$data[] = array('<BODY ONLOAD=alert(\'XSS\')>', '<BODY>');
|
||||
|
||||
// Event handlers.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Event_Handlers
|
||||
$events = array(
|
||||
'onAbort',
|
||||
'onActivate',
|
||||
'onAfterPrint',
|
||||
'onAfterUpdate',
|
||||
'onBeforeActivate',
|
||||
'onBeforeCopy',
|
||||
'onBeforeCut',
|
||||
'onBeforeDeactivate',
|
||||
'onBeforeEditFocus',
|
||||
'onBeforePaste',
|
||||
'onBeforePrint',
|
||||
'onBeforeUnload',
|
||||
'onBeforeUpdate',
|
||||
'onBegin',
|
||||
'onBlur',
|
||||
'onBounce',
|
||||
'onCellChange',
|
||||
'onChange',
|
||||
'onClick',
|
||||
'onContextMenu',
|
||||
'onControlSelect',
|
||||
'onCopy',
|
||||
'onCut',
|
||||
'onDataAvailable',
|
||||
'onDataSetChanged',
|
||||
'onDataSetComplete',
|
||||
'onDblClick',
|
||||
'onDeactivate',
|
||||
'onDrag',
|
||||
'onDragEnd',
|
||||
'onDragLeave',
|
||||
'onDragEnter',
|
||||
'onDragOver',
|
||||
'onDragDrop',
|
||||
'onDragStart',
|
||||
'onDrop',
|
||||
'onEnd',
|
||||
'onError',
|
||||
'onErrorUpdate',
|
||||
'onFilterChange',
|
||||
'onFinish',
|
||||
'onFocus',
|
||||
'onFocusIn',
|
||||
'onFocusOut',
|
||||
'onHashChange',
|
||||
'onHelp',
|
||||
'onInput',
|
||||
'onKeyDown',
|
||||
'onKeyPress',
|
||||
'onKeyUp',
|
||||
'onLayoutComplete',
|
||||
'onLoad',
|
||||
'onLoseCapture',
|
||||
'onMediaComplete',
|
||||
'onMediaError',
|
||||
'onMessage',
|
||||
'onMousedown',
|
||||
'onMouseEnter',
|
||||
'onMouseLeave',
|
||||
'onMouseMove',
|
||||
'onMouseOut',
|
||||
'onMouseOver',
|
||||
'onMouseUp',
|
||||
'onMouseWheel',
|
||||
'onMove',
|
||||
'onMoveEnd',
|
||||
'onMoveStart',
|
||||
'onOffline',
|
||||
'onOnline',
|
||||
'onOutOfSync',
|
||||
'onPaste',
|
||||
'onPause',
|
||||
'onPopState',
|
||||
'onProgress',
|
||||
'onPropertyChange',
|
||||
'onReadyStateChange',
|
||||
'onRedo',
|
||||
'onRepeat',
|
||||
'onReset',
|
||||
'onResize',
|
||||
'onResizeEnd',
|
||||
'onResizeStart',
|
||||
'onResume',
|
||||
'onReverse',
|
||||
'onRowsEnter',
|
||||
'onRowExit',
|
||||
'onRowDelete',
|
||||
'onRowInserted',
|
||||
'onScroll',
|
||||
'onSeek',
|
||||
'onSelect',
|
||||
'onSelectionChange',
|
||||
'onSelectStart',
|
||||
'onStart',
|
||||
'onStop',
|
||||
'onStorage',
|
||||
'onSyncRestored',
|
||||
'onSubmit',
|
||||
'onTimeError',
|
||||
'onTrackChange',
|
||||
'onUndo',
|
||||
'onUnload',
|
||||
'onURLFlip',
|
||||
);
|
||||
foreach ($events as $event) {
|
||||
$data[] = array('<p ' . $event . '="javascript:alert(\'XSS\');">Dangerous llama!</p>', '<p>Dangerous llama!</p>');
|
||||
}
|
||||
|
||||
// BGSOUND.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#BGSOUND
|
||||
$data[] = array('<BGSOUND SRC="javascript:alert(\'XSS\');">', '<BGSOUND src="alert('XSS');">');
|
||||
|
||||
// & JavaScript includes.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#.26_JavaScript_includes
|
||||
$data[] = array('<BR SIZE="&{alert(\'XSS\')}">', '<BR size="">');
|
||||
|
||||
// STYLE sheet.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#STYLE_sheet
|
||||
$data[] = array('<LINK REL="stylesheet" HREF="javascript:alert(\'XSS\');">', '');
|
||||
|
||||
// Remote style sheet.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Remote_style_sheet
|
||||
$data[] = array('<LINK REL="stylesheet" HREF="http://ha.ckers.org/xss.css">', '');
|
||||
|
||||
// Remote style sheet part 2.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Remote_style_sheet_part_2
|
||||
$data[] = array('<STYLE>@import\'http://ha.ckers.org/xss.css\';</STYLE>', '@import\'http://ha.ckers.org/xss.css\';');
|
||||
|
||||
// Remote style sheet part 3.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Remote_style_sheet_part_3
|
||||
$data[] = array('<META HTTP-EQUIV="Link" Content="<http://ha.ckers.org/xss.css>; REL=stylesheet">', '<META http-equiv="Link">; REL=stylesheet">');
|
||||
|
||||
// Remote style sheet part 4.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Remote_style_sheet_part_4
|
||||
$data[] = array('<STYLE>BODY{-moz-binding:url("http://ha.ckers.org/xssmoz.xml#xss")}</STYLE>', 'BODY{-moz-binding:url("http://ha.ckers.org/xssmoz.xml#xss")}');
|
||||
|
||||
// STYLE tags with broken up JavaScript for XSS.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#STYLE_tags_with_broken_up_JavaScript_for_XSS
|
||||
$data[] = array('<STYLE>@im\port\'\ja\vasc\ript:alert("XSS")\';</STYLE>', '@im\port\'\ja\vasc\ript:alert("XSS")\';');
|
||||
|
||||
// STYLE attribute using a comment to break up expression.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#STYLE_attribute_using_a_comment_to_break_up_expression
|
||||
$data[] = array('<IMG STYLE="xss:expr/*XSS*/ession(alert(\'XSS\'))">', '<IMG>');
|
||||
|
||||
// IMG STYLE with expression.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#IMG_STYLE_with_expression
|
||||
$data[] = array('exp/*<A STYLE=\'no\xss:noxss("*//*");
|
||||
xss:ex/*XSS*//*/*/pression(alert("XSS"))\'>', 'exp/*<A>');
|
||||
|
||||
// STYLE tag (Older versions of Netscape only).
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#STYLE_tag_.28Older_versions_of_Netscape_only.29
|
||||
$data[] = array('<STYLE TYPE="text/javascript">alert(\'XSS\');</STYLE>', 'alert(\'XSS\');');
|
||||
|
||||
// STYLE tag using background-image.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#STYLE_tag_using_background-image
|
||||
$data[] = array('<STYLE>.XSS{background-image:url("javascript:alert(\'XSS\')");}</STYLE><A CLASS=XSS></A>', '.XSS{background-image:url("javascript:alert(\'XSS\')");}<A class="XSS"></A>');
|
||||
|
||||
// STYLE tag using background.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#STYLE_tag_using_background
|
||||
$data[] = array('<STYLE type="text/css">BODY{background:url("javascript:alert(\'XSS\')")}</STYLE>', 'BODY{background:url("javascript:alert(\'XSS\')")}');
|
||||
|
||||
// Anonymous HTML with STYLE attribute.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Anonymous_HTML_with_STYLE_attribute
|
||||
$data[] = array('<XSS STYLE="xss:expression(alert(\'XSS\'))">', '<XSS>');
|
||||
|
||||
// Local htc file.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Local_htc_file
|
||||
$data[] = array('<XSS STYLE="behavior: url(xss.htc);">', '<XSS>');
|
||||
|
||||
// US-ASCII encoding.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#US-ASCII_encoding
|
||||
// This one is irrelevant for Drupal; Drupal *always* outputs UTF-8.
|
||||
|
||||
// META.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#META
|
||||
$data[] = array('<META HTTP-EQUIV="refresh" CONTENT="0;url=javascript:alert(\'XSS\');">', '<META http-equiv="refresh" content="alert('XSS');">');
|
||||
|
||||
// META using data.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#META_using_data
|
||||
$data[] = array('<META HTTP-EQUIV="refresh" CONTENT="0;url=data:text/html base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">', '<META http-equiv="refresh" content="text/html base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">');
|
||||
|
||||
// META with additional URL parameter
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#META
|
||||
$data[] = array('<META HTTP-EQUIV="refresh" CONTENT="0; URL=http://;URL=javascript:alert(\'XSS\');">', '<META http-equiv="refresh" content="//;URL=javascript:alert('XSS');">');
|
||||
|
||||
// IFRAME.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#IFRAME
|
||||
$data[] = array('<IFRAME SRC="javascript:alert(\'XSS\');"></IFRAME>', '<IFRAME src="alert('XSS');"></IFRAME>');
|
||||
|
||||
// IFRAME Event based.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#IFRAME_Event_based
|
||||
$data[] = array('<IFRAME SRC=# onmouseover="alert(document.cookie)"></IFRAME>', '<IFRAME src="#"></IFRAME>');
|
||||
|
||||
// FRAME.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#FRAME
|
||||
$data[] = array('<FRAMESET><FRAME SRC="javascript:alert(\'XSS\');"></FRAMESET>', '<FRAMESET><FRAME src="alert('XSS');"></FRAMESET>');
|
||||
|
||||
// TABLE.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#TABLE
|
||||
$data[] = array('<TABLE BACKGROUND="javascript:alert(\'XSS\')">', '<TABLE background="alert('XSS')">');
|
||||
|
||||
// TD.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#TD
|
||||
$data[] = array('<TABLE><TD BACKGROUND="javascript:alert(\'XSS\')">', '<TABLE><TD background="alert('XSS')">');
|
||||
|
||||
// DIV background-image.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#DIV_background-image
|
||||
$data[] = array('<DIV STYLE="background-image: url(javascript:alert(\'XSS\'))">', '<DIV>');
|
||||
|
||||
// DIV background-image with unicoded XSS exploit.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#DIV_background-image_with_unicoded_XSS_exploit
|
||||
$data[] = array('<DIV STYLE="background-image:\0075\0072\006C\0028\'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029\'\0029">', '<DIV>');
|
||||
|
||||
// DIV background-image plus extra characters.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#DIV_background-image_plus_extra_characters
|
||||
$data[] = array('<DIV STYLE="background-image: url(javascript:alert(\'XSS\'))">', '<DIV>');
|
||||
|
||||
// DIV expression.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#DIV_expression
|
||||
$data[] = array('<DIV STYLE="width: expression(alert(\'XSS\'));">', '<DIV>');
|
||||
|
||||
// Downlevel-Hidden block.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Downlevel-Hidden_block
|
||||
$data[] = array('<!--[if gte IE 4]>
|
||||
<SCRIPT>alert(\'XSS\');</SCRIPT>
|
||||
<![endif]-->', "\n alert('XSS');\n ");
|
||||
|
||||
// BASE tag.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#BASE_tag
|
||||
$data[] = array('<BASE HREF="javascript:alert(\'XSS\');//">', '<BASE href="alert('XSS');//">');
|
||||
|
||||
// OBJECT tag.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#OBJECT_tag
|
||||
$data[] = array('<OBJECT TYPE="text/x-scriptlet" DATA="http://ha.ckers.org/scriptlet.html"></OBJECT>', '');
|
||||
|
||||
// Using an EMBED tag you can embed a Flash movie that contains XSS.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Using_an_EMBED_tag_you_can_embed_a_Flash_movie_that_contains_XSS
|
||||
$data[] = array('<EMBED SRC="http://ha.ckers.org/xss.swf" AllowScriptAccess="always"></EMBED>', '');
|
||||
|
||||
// You can EMBED SVG which can contain your XSS vector.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#You_can_EMBED_SVG_which_can_contain_your_XSS_vector
|
||||
$data[] = array('<EMBED SRC="data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dH A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv MjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hs aW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAw IiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlh TUyIpOzwvc2NyaXB0Pjwvc3ZnPg==" type="image/svg+xml" AllowScriptAccess="always"></EMBED>', '');
|
||||
|
||||
// XML data island with CDATA obfuscation.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#XML_data_island_with_CDATA_obfuscation
|
||||
$data[] = array('<XML ID="xss"><I><B><IMG SRC="javas<!-- -->cript:alert(\'XSS\')"></B></I></XML><SPAN DATASRC="#xss" DATAFLD="B" DATAFORMATAS="HTML"></SPAN>', '<XML id="xss"><I><B><IMG>cript:alert(\'XSS\')"></B></I></XML><SPAN datasrc="#xss" datafld="B" dataformatas="HTML"></SPAN>');
|
||||
|
||||
// Locally hosted XML with embedded JavaScript that is generated using an XML data island.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Locally_hosted_XML_with_embedded_JavaScript_that_is_generated_using_an_XML_data_island
|
||||
// This one is irrelevant for Drupal; Drupal disallows XML uploads by
|
||||
// default.
|
||||
|
||||
// HTML+TIME in XML.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#HTML.2BTIME_in_XML
|
||||
$data[] = array('<?xml:namespace prefix="t" ns="urn:schemas-microsoft-com:time"><?import namespace="t" implementation="#default#time2"><t:set attributeName="innerHTML" to="XSS<SCRIPT DEFER>alert("XSS")</SCRIPT>">', '<?xml:namespace prefix="t" ns="urn:schemas-microsoft-com:time"><?import namespace="t" implementation="#default#time2"><t set attributename="innerHTML">alert("XSS")">');
|
||||
|
||||
// Assuming you can only fit in a few characters and it filters against ".js".
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Assuming_you_can_only_fit_in_a_few_characters_and_it_filters_against_.22.js.22
|
||||
$data[] = array('<SCRIPT SRC="http://ha.ckers.org/xss.jpg"></SCRIPT>', '');
|
||||
|
||||
// IMG Embedded commands.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#IMG_Embedded_commands
|
||||
// This one is irrelevant for Drupal; this is actually a CSRF, for which
|
||||
// Drupal has CSRF protection. See https://www.drupal.org/node/178896.
|
||||
|
||||
// Cookie manipulation.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#Cookie_manipulation
|
||||
$data[] = array('<META HTTP-EQUIV="Set-Cookie" Content="USERID=<SCRIPT>alert(\'XSS\')</SCRIPT>">', '<META http-equiv="Set-Cookie">alert(\'XSS\')">');
|
||||
|
||||
// UTF-7 encoding.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#UTF-7_encoding
|
||||
// This one is irrelevant for Drupal; Drupal *always* outputs UTF-8.
|
||||
|
||||
// XSS using HTML quote encapsulation.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#XSS_using_HTML_quote_encapsulation
|
||||
$data[] = array('<SCRIPT a=">" SRC="http://ha.ckers.org/xss.js"></SCRIPT>', '" SRC="http://ha.ckers.org/xss.js">');
|
||||
$data[] = array('<SCRIPT =">" SRC="http://ha.ckers.org/xss.js"></SCRIPT>', '" SRC="http://ha.ckers.org/xss.js">');
|
||||
$data[] = array('<SCRIPT a=">" \'\' SRC="http://ha.ckers.org/xss.js"></SCRIPT>', '" \'\' SRC="http://ha.ckers.org/xss.js">');
|
||||
$data[] = array('<SCRIPT "a=\'>\'" SRC="http://ha.ckers.org/xss.js"></SCRIPT>', '\'" SRC="http://ha.ckers.org/xss.js">');
|
||||
$data[] = array('<SCRIPT a=`>` SRC="http://ha.ckers.org/xss.js"></SCRIPT>', '` SRC="http://ha.ckers.org/xss.js">');
|
||||
$data[] = array('<SCRIPT a=">\'>" SRC="http://ha.ckers.org/xss.js"></SCRIPT>', '\'>" SRC="http://ha.ckers.org/xss.js">');
|
||||
$data[] = array('<SCRIPT>document.write("<SCRI");</SCRIPT>PT SRC="http://ha.ckers.org/xss.js"></SCRIPT>', 'document.write("<SCRI>PT SRC="http://ha.ckers.org/xss.js">');
|
||||
|
||||
// URL string evasion.
|
||||
// @see https://www.owasp.org/index.php/XSS_Filter_Evasion_Cheat_Sheet#URL_string_evasion
|
||||
// This one is irrelevant for Drupal; Drupal doesn't forbid linking to some
|
||||
// sites, it only forbids linking to any protocols other than those that are
|
||||
// whitelisted.
|
||||
|
||||
// Test XSS filtering on data-attributes.
|
||||
// @see \Drupal\editor\EditorXssFilter::filterXssDataAttributes()
|
||||
|
||||
// The following two test cases verify that XSS attack vectors are filtered.
|
||||
$data[] = array('<img src="butterfly.jpg" data-caption="<script>alert();</script>" />', '<img src="butterfly.jpg" data-caption="alert();" />');
|
||||
$data[] = array('<img src="butterfly.jpg" data-caption="<EMBED SRC="http://ha.ckers.org/xss.swf" AllowScriptAccess="always"></EMBED>" />', '<img src="butterfly.jpg" data-caption="" />');
|
||||
|
||||
// When including HTML-tags as visible content, they are double-escaped.
|
||||
// This test case ensures that we leave that content unchanged.
|
||||
$data[] = array('<img src="butterfly.jpg" data-caption="&lt;script&gt;alert();&lt;/script&gt;" />', '<img src="butterfly.jpg" data-caption="&lt;script&gt;alert();&lt;/script&gt;" />');
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the method for filtering XSS.
|
||||
*
|
||||
* @param string $input
|
||||
* The input.
|
||||
* @param string $expected_output
|
||||
* The expected output.
|
||||
*
|
||||
* @dataProvider providerTestFilterXss
|
||||
*/
|
||||
public function testFilterXss($input, $expected_output) {
|
||||
$output = Standard::filterXss($input, $this->format);
|
||||
$this->assertSame($expected_output, $output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests removing disallowed tags and XSS prevention.
|
||||
*
|
||||
* \Drupal\Component\Utility\Xss::filter() has the ability to run in blacklist
|
||||
* mode, in which it still applies the exact same filtering, with one
|
||||
* exception: it no longer works with a list of allowed tags, but with a list
|
||||
* of disallowed tags.
|
||||
*
|
||||
* @param string $value
|
||||
* The value to filter.
|
||||
* @param string $expected
|
||||
* The string that is expected to be missing.
|
||||
* @param string $message
|
||||
* The assertion message to display upon failure.
|
||||
* @param array $disallowed_tags
|
||||
* (optional) The disallowed HTML tags to be passed to \Drupal\Component\Utility\Xss::filter().
|
||||
*
|
||||
* @dataProvider providerTestBlackListMode
|
||||
*/
|
||||
public function testBlacklistMode($value, $expected, $message, array $disallowed_tags) {
|
||||
$value = Standard::filter($value, $disallowed_tags);
|
||||
$this->assertSame($expected, $value, $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testBlacklistMode().
|
||||
*
|
||||
* @see testBlacklistMode()
|
||||
*
|
||||
* @return array
|
||||
* An array of arrays containing the following elements:
|
||||
* - The value to filter.
|
||||
* - The value to expect after filtering.
|
||||
* - The assertion message.
|
||||
* - (optional) The disallowed HTML tags to be passed to \Drupal\Component\Utility\Xss::filter().
|
||||
*/
|
||||
public function providerTestBlackListMode() {
|
||||
return array(
|
||||
array(
|
||||
'<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',
|
||||
array('script')
|
||||
),
|
||||
array(
|
||||
'<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',
|
||||
array('script', 'video')
|
||||
),
|
||||
// No real use case for this, but it is an edge case we must ensure works.
|
||||
array(
|
||||
'<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',
|
||||
array()
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue