Move into nested docroot

This commit is contained in:
Rob Davies 2017-02-13 15:31:17 +00:00
parent 83a0d3a149
commit c8b70abde9
13405 changed files with 0 additions and 0 deletions

View file

@ -0,0 +1,42 @@
<?php
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,
);
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Drupal\editor\Ajax;
use Drupal\Core\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);
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace Drupal\editor\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines an Editor annotation object.
*
* Plugin Namespace: Plugin\Editor
*
* Text editor plugin implementations 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.
* - supported_element_types: On which form element #types this text editor is
* capable of working.
*
* 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,
* supported_element_types = {
* "textarea",
* "textfield",
* }
* )
* @endcode
*
* 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 hook_editor_info_alter()
* @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;
}

View file

@ -0,0 +1,24 @@
<?php
namespace Drupal\editor;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the text editor entity type.
*
* @see \Drupal\editor\Entity\Editor
*/
class EditorAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $editor, $operation, AccountInterface $account) {
/** @var \Drupal\editor\EditorInterface $editor */
return $editor->getFilterFormat()->access($operation, $account, TRUE);
}
}

View file

@ -0,0 +1,81 @@
<?php
namespace Drupal\editor;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\editor\Ajax\GetUntransformedTextCommand;
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 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.
*
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
* Thrown if no value to filter is specified.
*
* @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));
}
}

View file

@ -0,0 +1,85 @@
<?php
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);
}

View file

@ -0,0 +1,173 @@
<?php
namespace Drupal\editor\EditorXssFilter;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Xss;
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 = Html::escape($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);
}
}

View file

@ -0,0 +1,42 @@
<?php
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);
}

View file

@ -0,0 +1,118 @@
<?php
namespace Drupal\editor;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Render\BubbleableMetadata;
/**
* Defines a service for Text Editor's render elements.
*/
class Element {
/**
* The Text Editor plugin 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'] = BubbleableMetadata::mergeAttachments($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;
}
}

View file

@ -0,0 +1,186 @@
<?php
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"),
* handlers = {
* "access" = "Drupal\editor\EditorAccessControlHandler",
* },
* 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;
}
/**
* {@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;
}
}

View file

@ -0,0 +1,239 @@
<?php
namespace Drupal\editor\Form;
use Drupal\Component\Utility\Bytes;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\editor\Entity\Editor;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\editor\Ajax\EditorDialogSave;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
/**
* Provides an image dialog for text editors.
*/
class EditorImageDialog extends FormBase {
/**
* The file storage service.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $fileStorage;
/**
* Constructs a form object for image dialog.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $file_storage
* The file storage service.
*/
public function __construct(EntityStorageInterface $file_storage) {
$this->fileStorage = $file_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity.manager')->getStorage('file')
);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'editor_image_dialog';
}
/**
* {@inheritdoc}
*
* @param \Drupal\editor\Entity\Editor $editor
* The text editor to which this dialog corresponds.
*/
public function buildForm(array $form, FormStateInterface $form_state, Editor $editor = NULL) {
// This form is special, in that the default values do not come from the
// server side, but from the client side, from a text editor. We must cache
// this data in form state, because when the form is rebuilt, we will be
// receiving values from the form, instead of the values from the text
// editor. If we don't cache it, this data will be lost.
if (isset($form_state->getUserInput()['editor_object'])) {
// By convention, the data that the text editor sends to any dialog is in
// the 'editor_object' key. And the image dialog for text editors expects
// that data to be the attributes for an <img> element.
$image_element = $form_state->getUserInput()['editor_object'];
$form_state->set('image_element', $image_element);
$form_state->setCached(TRUE);
}
else {
// Retrieve the image element's attributes from form state.
$image_element = $form_state->get('image_element') ?: [];
}
$form['#tree'] = TRUE;
$form['#attached']['library'][] = 'editor/drupal.editor.dialog';
$form['#prefix'] = '<div id="editor-image-dialog-form">';
$form['#suffix'] = '</div>';
// Construct strings to use in the upload validators.
$image_upload = $editor->getImageUploadSettings();
if (!empty($image_upload['max_dimensions']['width']) || !empty($image_upload['max_dimensions']['height'])) {
$max_dimensions = $image_upload['max_dimensions']['width'] . 'x' . $image_upload['max_dimensions']['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,
);
// 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']) && $editor->getFilterFormat()->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']) && $editor->getFilterFormat()->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 = $this->fileStorage->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;
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace Drupal\editor\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\editor\Entity\Editor;
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\editor\Entity\Editor $editor
* The text editor to which this dialog corresponds.
*/
public function buildForm(array $form, FormStateInterface $form_state, Editor $editor = 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['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;
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace Drupal\editor\Plugin;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\editor\Entity\Editor;
/**
* 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 specify an annotation containing the
* plugin definition so the plugin can be discovered.
*
* @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) {
}
}

View file

@ -0,0 +1,99 @@
<?php
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\Core\Render\AttachmentsResponseProcessorInterface::processAttachments()
*/
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;
}
}

View file

@ -0,0 +1,117 @@
<?php
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\Core\Render\AttachmentsResponseProcessorInterface::processAttachments()
* @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\Core\Render\AttachmentsResponseProcessorInterface::processAttachments()
* @see EditorManager::getAttachments()
*/
public function getLibraries(Editor $editor);
}

View file

@ -0,0 +1,100 @@
<?php
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.
*
* Generates file URLs and 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');
// If there is a 'src' attribute, set it to the file entity's current
// URL. This ensures the URL works even after the file location changes.
if ($node->hasAttribute('src')) {
$file = $this->entityManager->loadEntityByUuid('file', $uuid);
if ($file) {
$node->setAttribute('src', file_url_transform_relative(file_create_url($file->getFileUri())));
}
}
// 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());
}
}
}
$result->setProcessedText(Html::serialize($dom));
}
return $result;
}
}

View file

@ -0,0 +1,96 @@
<?php
namespace Drupal\editor\Plugin\InPlaceEditor;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\filter\Entity\FilterFormat;
use Drupal\quickedit\Plugin\InPlaceEditorInterface;
use Drupal\filter\Plugin\FilterInterface;
/**
* Defines the formatted text in-place editor.
*
* @InPlaceEditor(
* id = "editor"
* )
*/
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 ($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.
*
* @param int $format_id
* A text format ID.
*
* @return bool
*/
protected function textFormatHasTransformationFilters($format_id) {
$format = FilterFormat::load($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;
}
}

View file

@ -0,0 +1,236 @@
<?php
namespace Drupal\editor\Tests;
use Drupal\Component\Utility\Unicode;
use Drupal\filter\Entity\FilterFormat;
use Drupal\node\Entity\Node;
use Drupal\node\Entity\NodeType;
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 = FilterFormat::create(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->addEditorToNewFormat('monocerus', 'Monocerus');
$this->verifyUnicornEditorConfiguration('monocerus');
}
/**
* Tests format disabling.
*/
public function testDisableFormatWithEditor() {
$formats = ['monocerus' => 'Monocerus', 'tattoo' => 'Tattoo'];
// Install the node module.
$this->container->get('module_installer')->install(['node']);
$this->resetAll();
// Create a new node type and attach the 'body' field to it.
$node_type = NodeType::create(['type' => Unicode::strtolower($this->randomMachineName())]);
$node_type->save();
node_add_body_field($node_type, $this->randomString());
$permissions = ['administer filters', "edit any {$node_type->id()} content"];
foreach ($formats as $format => $name) {
// Create a format and add an editor to this format.
$this->addEditorToNewFormat($format, $name);
// Add permission for this format.
$permissions[] = "use text format $format";
}
// Create a node having the body format value 'moncerus'.
$node = Node::create([
'type' => $node_type->id(),
'title' => $this->randomString(),
]);
$node->body->value = $this->randomString(100);
$node->body->format = 'monocerus';
$node->save();
// Log in as an user able to use both formats and edit nodes of created type.
$account = $this->drupalCreateUser($permissions);
$this->drupalLogin($account);
// The node edit page header.
$text = t('<em>Edit @type</em> @title', array('@type' => $node_type->label(), '@title' => $node->label()));
// Go to node edit form.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertRaw($text);
// Disable the format assigned to the 'body' field of the node.
FilterFormat::load('monocerus')->disable()->save();
// Edit again the node.
$this->drupalGet('node/' . $node->id() . '/edit');
$this->assertRaw($text);
}
/**
* Adds an editor to a new format using the UI.
*
* @param string $format_id
* The format id.
* @param string $format_name
* The format name.
*/
protected function addEditorToNewFormat($format_id, $format_name) {
$this->enableUnicornEditor();
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/content/formats/add');
// Configure the text format name.
$edit = array(
'name' => $format_name,
'format' => $format_id,
);
$edit += $this->selectUnicornEditor();
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
}
/**
* Enables the unicorn editor.
*/
protected function enableUnicornEditor() {
if (!$this->container->get('module_handler')->moduleExists('editor_test')) {
$this->container->get('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.');
}
}

View file

@ -0,0 +1,83 @@
<?php
namespace Drupal\editor\Tests;
use Drupal\Core\Url;
use Drupal\editor\Entity\Editor;
use Drupal\Tests\BrowserTestBase;
/**
* Test access to the editor dialog forms.
*
* @group editor
*/
class EditorDialogAccessTest extends BrowserTestBase {
/**
* Modules to install.
*
* @var array
*/
public static $modules = ['editor', 'filter', 'ckeditor'];
/**
* Test access to the editor image dialog.
*/
public function testEditorImageDialogAccess() {
$url = Url::fromRoute('editor.image_dialog', ['editor' => 'plain_text']);
$session = $this->assertSession();
// With no text editor, expect a 404.
$this->drupalGet($url);
$session->statusCodeEquals(404);
// With a text editor but without image upload settings, expect a 200, but
// there should not be an input[type=file].
$editor = Editor::create([
'editor' => 'ckeditor',
'format' => 'plain_text',
'settings' => [
'toolbar' => [
'rows' => [
[
[
'name' => 'Media',
'items' => [
'DrupalImage',
],
],
],
],
],
'plugins' => [],
],
'image_upload' => [
'status' => FALSE,
'scheme' => 'public',
'directory' => 'inline-images',
'max_size' => '',
'max_dimensions' => [
'width' => 0,
'height' => 0,
],
],
]);
$editor->save();
$this->resetAll();
$this->drupalGet($url);
$this->assertTrue($this->cssSelect('input[type=text][name="attributes[src]"]'), 'Image uploads disabled: input[type=text][name="attributes[src]"] is present.');
$this->assertFalse($this->cssSelect('input[type=file]'), 'Image uploads disabled: input[type=file] is absent.');
$session->statusCodeEquals(200);
// With image upload settings, expect a 200, and now there should be an
// input[type=file].
$editor->setImageUploadSettings(['status' => TRUE] + $editor->getImageUploadSettings())
->save();
$this->resetAll();
$this->drupalGet($url);
$this->assertFalse($this->cssSelect('input[type=text][name="attributes[src]"]'), 'Image uploads enabled: input[type=text][name="attributes[src]"] is absent.');
$this->assertTrue($this->cssSelect('input[type=file]'), 'Image uploads enabled: input[type=file] is present.');
$session->statusCodeEquals(200);
}
}

View file

@ -0,0 +1,285 @@
<?php
namespace Drupal\editor\Tests;
use Drupal\editor\Entity\Editor;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\simpletest\WebTestBase;
use Drupal\filter\Entity\FilterFormat;
/**
* 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 = FilterFormat::create(array(
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
'filters' => array(),
));
$filtered_html_format->save();
$full_html_format = FilterFormat::create(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 = Editor::create([
'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 = Editor::create([
'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 = Editor::create([
'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();
Editor::create([
'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")]'),
);
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace Drupal\editor\Tests;
use Drupal\file\Entity\File;
use Drupal\Tests\BrowserTestBase;
use Drupal\user\Entity\Role;
use Drupal\user\RoleInterface;
/**
* Tests Editor module's file reference filter with private files.
*
* @group editor
*/
class EditorPrivateFileReferenceFilterTest extends BrowserTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = [
// Needed for the config: this is the only module in core that utilizes the
// functionality in editor.module to be tested, and depends on that.
'ckeditor',
// Depends on filter.module (indirectly).
'node',
// Pulls in the config we're using during testing which create a text format
// - with the filter_html_image_secure filter DISABLED,
// - with the editor set to CKEditor,
// - with drupalimage.image_upload.scheme set to 'private',
// - with drupalimage.image_upload.directory set to ''.
'editor_private_test',
];
/**
* Tests the editor file reference filter with private files.
*/
function testEditorPrivateFileReferenceFilter() {
$author = $this->drupalCreateUser();
$this->drupalLogin($author);
// Create a content type with a body field.
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
// Create a file in the 'private:// ' stream.
$filename = 'test.png';
$src = '/system/files/' . $filename;
/** @var \Drupal\file\FileInterface $file */
$file = File::create([
'uri' => 'private://' . $filename,
]);
$file->setTemporary();
$file->setOwner($author);
// Create the file itself.
file_put_contents($file->getFileUri(), $this->randomString());
$file->save();
// The image should be visible for its author.
$this->drupalGet($src);
$this->assertSession()->statusCodeEquals(200);
// The not-yet-permanent image should NOT be visible for anonymous.
$this->drupalLogout();
$this->drupalGet($src);
$this->assertSession()->statusCodeEquals(403);
// Resave the file to be permanent.
$file->setPermanent();
$file->save();
// Create a node with its body field properly pointing to the just-created
// file.
$node = $this->drupalCreateNode([
'type' => 'page',
'body' => [
'value' => '<img alt="alt" data-entity-type="file" data-entity-uuid="' . $file->uuid() . '" src="' . $src . '" />',
'format' => 'private_images',
],
'uid' => $author->id(),
]);
// Do the actual test. The image should be visible for anonymous users,
// because they can view the referencing entity.
$this->drupalGet($node->toUrl());
$this->assertSession()->statusCodeEquals(200);
$this->drupalGet($src);
$this->assertSession()->statusCodeEquals(200);
// Disallow anonymous users to view the entity, which then should also
// disallow them to view the image.
Role::load(RoleInterface::ANONYMOUS_ID)
->revokePermission('access content')
->save();
$this->drupalGet($node->toUrl());
$this->assertSession()->statusCodeEquals(403);
$this->drupalGet($src);
$this->assertSession()->statusCodeEquals(403);
}
}

View file

@ -0,0 +1,435 @@
<?php
namespace Drupal\editor\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\editor\Entity\Editor;
use Drupal\simpletest\WebTestBase;
use Drupal\filter\Entity\FilterFormat;
/**
* 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 = FilterFormat::create(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' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
)
),
),
));
$format->save();
$format = FilterFormat::create(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' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
)
),
),
));
$format->save();
$editor = Editor::create([
'format' => 'restricted_with_editor',
'editor' => 'unicorn',
]);
$editor->save();
$format = FilterFormat::create(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' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a> <embed>',
)
),
),
));
$format->save();
$editor = Editor::create([
'format' => 'restricted_plus_dangerous_tag_with_editor',
'editor' => 'unicorn',
]);
$editor->save();
$format = FilterFormat::create(array(
'format' => 'unrestricted_without_editor',
'name' => 'Unrestricted HTML, without text editor',
'weight' => 0,
'filters' => array(),
));
$format->save();
$format = FilterFormat::create(array(
'format' => 'unrestricted_with_editor',
'name' => 'Unrestricted HTML, with text editor',
'weight' => 1,
'filters' => array(),
));
$format->save();
$editor = Editor::create([
'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
$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.');
}
}

View file

@ -0,0 +1,223 @@
<?php
namespace Drupal\editor\Tests;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\simpletest\WebTestBase;
/**
* Tests scaling of inline images.
*
* @group editor
*/
class EditorUploadImageScaleTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = ['editor', 'editor_test'];
/**
* A user with permission as administer for testing.
*
* @var \Drupal\user\Entity\User
*/
protected $adminUser;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
// Add text format.
FilterFormat::create([
'format' => 'basic_html',
'name' => 'Basic HTML',
'weight' => 0,
])->save();
// Set up text editor.
Editor::create([
'format' => 'basic_html',
'editor' => 'unicorn',
'image_upload' => [
'status' => TRUE,
'scheme' => 'public',
'directory' => 'inline-images',
'max_size' => '',
'max_dimensions' => [
'width' => NULL,
'height' => NULL
],
]
])->save();
// Create admin user.
$this->adminUser = $this->drupalCreateUser(['administer filters', 'use text format basic_html']);
$this->drupalLogin($this->adminUser);
}
/**
* Tests scaling of inline images.
*/
public function testEditorUploadImageScale() {
// Generate testing images.
$testing_image_list = $this->drupalGetTestFiles('image');
// Case 1: no max dimensions set: uploaded image not scaled.
$test_image = $testing_image_list[0];
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
$max_width = NULL;
$max_height = NULL;
$this->setMaxDimensions($max_width, $max_height);
$this->assertSavedMaxDimensions($max_width, $max_height);
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $image_file_width);
$this->assertEqual($uploaded_image_file_height, $image_file_height);
$this->assertNoRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
// Case 2: max width smaller than uploaded image: image scaled down.
$test_image = $testing_image_list[1];
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
$max_width = $image_file_width - 5;
$max_height = $image_file_height;
$this->setMaxDimensions($max_width, $max_height);
$this->assertSavedMaxDimensions($max_width, $max_height);
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $max_width);
$this->assertEqual($uploaded_image_file_height, $uploaded_image_file_height * ($uploaded_image_file_width / $max_width));
$this->assertRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
// Case 3: max height smaller than uploaded image: image scaled down.
$test_image = $testing_image_list[2];
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
$max_width = $image_file_width;
$max_height = $image_file_height - 5;
$this->setMaxDimensions($max_width, $max_height);
$this->assertSavedMaxDimensions($max_width, $max_height);
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $uploaded_image_file_width * ($uploaded_image_file_height / $max_height));
$this->assertEqual($uploaded_image_file_height, $max_height);
$this->assertRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
// Case 4: max dimensions greater than uploaded image: image not scaled.
$test_image = $testing_image_list[3];
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
$max_width = $image_file_width + 5;
$max_height = $image_file_height + 5;
$this->setMaxDimensions($max_width, $max_height);
$this->assertSavedMaxDimensions($max_width, $max_height);
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $image_file_width);
$this->assertEqual($uploaded_image_file_height, $image_file_height);
$this->assertNoRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
// Case 5: only max width dimension was provided and it was smaller than
// uploaded image: image scaled down.
$test_image = $testing_image_list[4];
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
$max_width = $image_file_width - 5;
$max_height = NULL;
$this->setMaxDimensions($max_width, $max_height);
$this->assertSavedMaxDimensions($max_width, $max_height);
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $max_width);
$this->assertEqual($uploaded_image_file_height, $uploaded_image_file_height * ($uploaded_image_file_width / $max_width));
$this->assertRaw(t('The image was resized to fit within the maximum allowed width of %width pixels.', ['%width' => $max_width]));
// Case 6: only max height dimension was provided and it was smaller than
// uploaded image: image scaled down.
$test_image = $testing_image_list[5];
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
$max_width = NULL;
$max_height = $image_file_height - 5;
$this->setMaxDimensions($max_width, $max_height);
$this->assertSavedMaxDimensions($max_width, $max_height);
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
$this->assertEqual($uploaded_image_file_width, $uploaded_image_file_width * ($uploaded_image_file_height / $max_height));
$this->assertEqual($uploaded_image_file_height, $max_height);
$this->assertRaw(t('The image was resized to fit within the maximum allowed height of %height pixels.', ['%height' => $max_height]));
}
/**
* Gets the dimensions of an uploaded image.
*
* @param string $uri
* The URI of the image.
*
* @return array
* An array containing the uploaded image's width and height.
*/
protected function getTestImageInfo($uri) {
$image_file = $this->container->get('image.factory')->get($uri);
return [
(int) $image_file->getWidth(),
(int) $image_file->getHeight(),
];
}
/**
* Sets the maximum dimensions and saves the configuration.
*
* @param string|int $width
* The width of the image.
* @param string|int $height
* The height of the image.
*/
protected function setMaxDimensions($width, $height) {
$editor = Editor::load('basic_html');
$image_upload_settings = $editor->getImageUploadSettings();
$image_upload_settings['max_dimensions']['width'] = $width;
$image_upload_settings['max_dimensions']['height'] = $height;
$editor->setImageUploadSettings($image_upload_settings);
$editor->save();
}
/**
* Uploads an image via the editor dialog.
*
* @param string $uri
* The URI of the image.
*
* @return array
* An array containing the uploaded image's width and height.
*/
protected function uploadImage($uri) {
$edit = [
'files[fid]' => drupal_realpath($uri),
];
$this->drupalGet('editor/dialog/image/basic_html');
$this->drupalPostForm('editor/dialog/image/basic_html', $edit, t('Upload'));
$uploaded_image_file = $this->container->get('image.factory')->get('public://inline-images/' . basename($uri));
return [
(int) $uploaded_image_file->getWidth(),
(int) $uploaded_image_file->getHeight(),
];
}
/**
* Asserts whether the saved maximum dimensions equal the ones provided.
*
* @param string $width
* The expected width of the uploaded image.
* @param string $height
* The expected height of the uploaded image.
*
* @return bool
*/
protected function assertSavedMaxDimensions($width, $height) {
$image_upload_settings = Editor::load('basic_html')->getImageUploadSettings();
$expected = [
'width' => $image_upload_settings['max_dimensions']['width'],
'height' => $image_upload_settings['max_dimensions']['height'],
];
$same_width = $this->assertEqual($width, $expected['width'], 'Actual width of "' . $width . '" equals the expected width of "' . $expected['width'] . '"');
$same_height = $this->assertEqual($height, $expected['height'], 'Actual height of "' . $height . '" equals the expected width of "' . $expected['height'] . '"');
return $same_width && $same_height;
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace Drupal\editor\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\simpletest\WebTestBase;
use Drupal\filter\Entity\FilterFormat;
/**
* 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 = FilterFormat::create(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 role="group" 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 role="group" class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
$response = $this->drupalPost('editor/' . 'node/1/body/en/full', '', [], ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
$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.');
}
}

View file

@ -0,0 +1,70 @@
<?php
namespace Drupal\editor\Tests\Update;
use Drupal\system\Tests\Update\UpdatePathTestBase;
/**
* Tests Editor module database updates.
*
* @group editor
*/
class EditorUpdateTest extends UpdatePathTestBase {
/**
* {@inheritdoc}
*/
public function setDatabaseDumpFiles() {
$this->databaseDumpFiles = [
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
// Simulate an un-synchronized environment.
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.editor-editor_update_8001.php',
];
}
/**
* Tests editor_update_8001().
*
* @see editor_update_8001()
*/
public function testEditorUpdate8001() {
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
$config_factory = $this->container->get('config.factory');
$format_basic_html = $config_factory->get('filter.format.basic_html');
$editor_basic_html = $config_factory->get('editor.editor.basic_html');
$format_full_html = $config_factory->get('filter.format.full_html');
$editor_full_html = $config_factory->get('editor.editor.full_html');
// Checks if the 'basic_html' format and editor statuses differ.
$this->assertTrue($format_basic_html->get('status'));
$this->assertFalse($editor_basic_html->get('status'));
$this->assertNotIdentical($format_basic_html->get('status'), $editor_basic_html->get('status'));
// Checks if the 'full_html' format and editor statuses differ.
$this->assertFalse($format_full_html->get('status'));
$this->assertTrue($editor_full_html->get('status'));
$this->assertNotIdentical($format_full_html->get('status'), $editor_full_html->get('status'));
// Run updates.
$this->runUpdates();
// Reload text formats and editors.
$format_basic_html = $config_factory->get('filter.format.basic_html');
$editor_basic_html = $config_factory->get('editor.editor.basic_html');
$format_full_html = $config_factory->get('filter.format.full_html');
$editor_full_html = $config_factory->get('editor.editor.full_html');
// Checks if the 'basic_html' format and editor statuses are in sync.
$this->assertTrue($format_basic_html->get('status'));
$this->assertTrue($editor_basic_html->get('status'));
$this->assertIdentical($format_basic_html->get('status'), $editor_basic_html->get('status'));
// Checks if the 'full_html' format and editor statuses are in sync.
$this->assertFalse($format_full_html->get('status'));
$this->assertFalse($editor_full_html->get('status'));
$this->assertIdentical($format_full_html->get('status'), $editor_full_html->get('status'));
}
}