Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,47 @@
<?php
/**
* @file
* Contains \Drupal\editor\Ajax\EditorDialogSave.
*/
namespace Drupal\editor\Ajax;
use Drupal\Core\Ajax\CommandInterface;
/**
* Provides an AJAX command for saving the contents of an editor dialog.
*
* This command is implemented in editor.dialog.js in
* Drupal.AjaxCommands.prototype.editorDialogSave.
*/
class EditorDialogSave implements CommandInterface {
/**
* An array of values that will be passed back to the editor by the dialog.
*
* @var string
*/
protected $values;
/**
* Constructs a EditorDialogSave object.
*
* @param string $values
* The values that should be passed to the form constructor in Drupal.
*/
public function __construct($values) {
$this->values = $values;
}
/**
* {@inheritdoc}
*/
public function render() {
return array(
'command' => 'editorDialogSave',
'values' => $this->values,
);
}
}

View file

@ -0,0 +1,29 @@
<?php
/**
* @file
* Contains \Drupal\editor\Ajax\GetUntransformedTextCommand.
*/
namespace Drupal\editor\Ajax;
use Drupal\Core\Ajax\CommandInterface;
use Drupal\quickedit\Ajax\BaseCommand;
/**
* AJAX command to rerender a formatted text field without any transformation
* filters.
*/
class GetUntransformedTextCommand extends BaseCommand {
/**
* Constructs a GetUntransformedTextCommand object.
*
* @param string $data
* The data to pass on to the client side.
*/
public function __construct($data) {
parent::__construct('editorGetUntransformedText', $data);
}
}

View file

@ -0,0 +1,72 @@
<?php
/**
* @file
* Contains \Drupal\editor\Annotation\Editor.
*/
namespace Drupal\editor\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines an Editor annotation object.
*
* Plugin Namespace: Plugin\Editor
*
* For a working example, see \Drupal\ckeditor\Plugin\Editor\CKEditor
*
* @see \Drupal\editor\Plugin\EditorPluginInterface
* @see \Drupal\editor\Plugin\EditorBase
* @see \Drupal\editor\Plugin\EditorManager
* @see plugin_api
*
* @Annotation
*/
class Editor extends Plugin {
/**
* The plugin ID.
*
* @var string
*/
public $id;
/**
* The human-readable name of the editor plugin.
*
* @ingroup plugin_translatable
*
* @var \Drupal\Core\Annotation\Translation
*/
public $label;
/**
* Whether the editor supports "allowed content only" filtering.
*
* @var bool
*/
public $supports_content_filtering;
/**
* Whether the editor supports the inline editing provided by the Edit module.
*
* @var bool
*/
public $supports_inline_editing;
/**
* Whether this text editor is not vulnerable to XSS attacks.
*
* @var bool
*/
public $is_xss_safe;
/**
* A list of element types this text editor supports.
*
* @var string[]
*/
public $supported_element_types;
}

View file

@ -0,0 +1,87 @@
<?php
/**
* @file
* Contains \Drupal\editor\EditorController.
*/
namespace Drupal\editor;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\OpenModalDialogCommand;
use Drupal\Core\Ajax\CloseModalDialogCommand;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\editor\Ajax\GetUntransformedTextCommand;
use Drupal\editor\Form\EditorImageDialog;
use Drupal\editor\Form\EditorLinkDialog;
use Drupal\filter\Plugin\FilterInterface;
use Drupal\filter\FilterFormatInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Returns responses for Editor module routes.
*/
class EditorController extends ControllerBase {
/**
* Returns an Ajax response to render a text field without transformation filters.
*
* @param \Drupal\Core\Entity\EntityInterface $entity
* The entity of which a formatted text field is being rerendered.
* @param string $field_name
* The name of the (formatted text) field that that is being rerendered
* @param string $langcode
* The name of the language for which the formatted text field is being
* rerendered.
* @param string $view_mode_id
* The view mode the formatted text field should be rerendered in.
*
* @return \Drupal\Core\Ajax\AjaxResponse
* The Ajax response.
*/
public function getUntransformedText(EntityInterface $entity, $field_name, $langcode, $view_mode_id) {
$response = new AjaxResponse();
// Direct text editing is only supported for single-valued fields.
$field = $entity->getTranslation($langcode)->$field_name;
$editable_text = check_markup($field->value, $field->format, $langcode, array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE));
$response->addCommand(new GetUntransformedTextCommand($editable_text));
return $response;
}
/**
* Apply the necessary XSS filtering for using a certain text format's editor.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
* @param \Drupal\filter\FilterFormatInterface $filter_format
* The text format whose text editor (if any) will be used.
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* A JSON response containing the XSS-filtered value.
*
* @see editor_filter_xss()
*/
public function filterXss(Request $request, FilterFormatInterface $filter_format) {
$value = $request->request->get('value');
if (!isset($value)) {
throw new NotFoundHttpException();
}
// The original_format parameter will only exist when switching text format.
$original_format_id = $request->request->get('original_format_id');
$original_format = NULL;
if (isset($original_format_id)) {
$original_format = $this->entityManager()
->getStorage('filter_format')
->load($original_format_id);
}
return new JsonResponse(editor_filter_xss($value, $filter_format, $original_format));
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* @file
* Contains \Drupal\editor\EditorInterface.
*/
namespace Drupal\editor;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining a text editor entity.
*/
interface EditorInterface extends ConfigEntityInterface {
/**
* Returns whether this text editor has an associated filter format.
*
* A text editor may be created at the same time as the filter format it's
* going to be associated with; in that case, no filter format object is
* available yet.
*
* @return bool
*/
public function hasAssociatedFilterFormat();
/**
* Returns the filter format this text editor is associated with.
*
* This could be NULL if the associated filter format is still being created.
* @see hasAssociatedFilterFormat()
*
* @return \Drupal\filter\FilterFormatInterface|null
*/
public function getFilterFormat();
/**
* Returns the associated text editor plugin ID.
*
* @return string
* The text editor plugin ID.
*/
public function getEditor();
/**
* Set the text editor plugin ID.
*
* @param string $editor
* The text editor plugin ID to set.
*/
public function setEditor($editor);
/**
* Returns the text editor plugin-specific settings.
*
* @return array
* A structured array containing all text editor settings.
*/
public function getSettings();
/**
* Sets the text editor plugin-specific settings.
*
* @param array $settings
* The structured array containing all text editor settings.
*
* @return $this
*/
public function setSettings(array $settings);
/**
* Returns the image upload settings.
*
* @return array
* A structured array containing image upload settings.
*/
public function getImageUploadSettings();
/**
* Sets the image upload settings.
*
* @param array $image_upload
* The structured array containing image upload settings.
*
* @return $this
*/
public function setImageUploadSettings(array $image_upload);
}

View file

@ -0,0 +1,179 @@
<?php
/**
* @file
* Contains \Drupal\editor\EditorXssFilter\Standard.
*/
namespace Drupal\editor\EditorXssFilter;
use Drupal\Component\Utility\Html;
use Drupal\Component\Utility\Xss;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\filter\FilterFormatInterface;
use Drupal\editor\EditorXssFilterInterface;
/**
* Defines the standard text editor XSS filter.
*/
class Standard extends Xss implements EditorXssFilterInterface {
/**
* {@inheritdoc}
*/
public static function filterXss($html, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL) {
// Apply XSS filtering, but blacklist the <script>, <style>, <link>, <embed>
// and <object> tags.
// The <script> and <style> tags are blacklisted because their contents
// can be malicious (and therefor they are inherently unsafe), whereas for
// all other tags, only their attributes can make them malicious. Since
// \Drupal\Component\Utility\Xss::filter() protects against malicious
// attributes, we take no blacklisting action.
// The exceptions to the above rule are <link>, <embed> and <object>:
// - <link> because the href attribute allows the attacker to import CSS
// using the HTTP(S) protocols which Xss::filter() considers safe by
// default. The imported remote CSS is applied to the main document, thus
// allowing for the same XSS attacks as a regular <style> tag.
// - <embed> and <object> because these tags allow non-HTML applications or
// content to be embedded using the src or data attributes, respectively.
// This is safe in the case of HTML documents, but not in the case of
// Flash objects for example, that may access/modify the main document
// directly.
// <iframe> is considered safe because it only allows HTML content to be
// embedded, hence ensuring the same origin policy always applies.
$dangerous_tags = array('script', 'style', 'link', 'embed', 'object');
// Simply blacklisting these five dangerous tags would bring safety, but
// also user frustration: what if a text format is configured to allow
// <embed>, for example? Then we would strip that tag, even though it is
// allowed, thereby causing data loss!
// Therefor, we want to be smarter still. We want to take into account which
// HTML tags are allowed and forbidden by the text format we're filtering
// for, and if we're switching from another text format, we want to take
// that format's allowed and forbidden tags into account as well.
// In other words: we only expect markup allowed in both the original and
// the new format to continue to exist.
$format_restrictions = $format->getHtmlRestrictions();
if ($original_format !== NULL) {
$original_format_restrictions = $original_format->getHtmlRestrictions();
}
// Any tags that are explicitly blacklisted by the text format must be
// appended to the list of default dangerous tags: if they're explicitly
// forbidden, then we must respect that configuration.
// When switching from another text format, we must use the union of
// forbidden tags: if either text format is more restrictive, then the
// safety expectations of *both* text formats apply.
$forbidden_tags = self::getForbiddenTags($format_restrictions);
if ($original_format !== NULL) {
$forbidden_tags = array_merge($forbidden_tags, self::getForbiddenTags($original_format_restrictions));
}
// Any tags that are explicitly whitelisted by the text format must be
// removed from the list of default dangerous tags: if they're explicitly
// allowed, then we must respect that configuration.
// When switching from another format, we must use the intersection of
// allowed tags: if either format is more restrictive, then the safety
// expectations of *both* formats apply.
$allowed_tags = self::getAllowedTags($format_restrictions);
if ($original_format !== NULL) {
$allowed_tags = array_intersect($allowed_tags, self::getAllowedTags($original_format_restrictions));
}
// Don't blacklist dangerous tags that are explicitly allowed in both text
// formats.
$blacklisted_tags = array_diff($dangerous_tags, $allowed_tags);
// Also blacklist tags that are explicitly forbidden in either text format.
$blacklisted_tags = array_merge($blacklisted_tags, $forbidden_tags);
$output = static::filter($html, $blacklisted_tags);
// Since data-attributes can contain encoded HTML markup that could be
// decoded and interpreted by editors, we need to apply XSS filtering to
// their contents.
return static::filterXssDataAttributes($output);
}
/**
* Applies a very permissive XSS/HTML filter to data-attributes.
*
* @param string $html
* The string to apply the data-attributes filtering to.
*
* @return string
* The filtered string.
*/
protected static function filterXssDataAttributes($html) {
if (stristr($html, 'data-') !== FALSE) {
$dom = Html::load($html);
$xpath = new \DOMXPath($dom);
foreach ($xpath->query('//@*[starts-with(name(.), "data-")]') as $node) {
// The data-attributes contain an HTML-encoded value, so we need to
// decode the value, apply XSS filtering and then re-save as encoded
// value. There is no need to explicitly decode $node->value, since the
// DOMAttr::value getter returns the decoded value.
$value = Xss::filterAdmin($node->value);
$node->value = SafeMarkup::checkPlain($value);
}
$html = Html::serialize($dom);
}
return $html;
}
/**
* Get all allowed tags from a restrictions data structure.
*
* @param array|FALSE $restrictions
* Restrictions as returned by FilterInterface::getHTMLRestrictions().
*
* @return array
* An array of allowed HTML tags.
*
* @see \Drupal\filter\Plugin\Filter\FilterInterface::getHTMLRestrictions()
*/
protected static function getAllowedTags($restrictions) {
if ($restrictions === FALSE || !isset($restrictions['allowed'])) {
return array();
}
$allowed_tags = array_keys($restrictions['allowed']);
// Exclude the wildcard tag, which is used to set attribute restrictions on
// all tags simultaneously.
$allowed_tags = array_diff($allowed_tags, array('*'));
return $allowed_tags;
}
/**
* Get all forbidden tags from a restrictions data structure.
*
* @param array|FALSE $restrictions
* Restrictions as returned by FilterInterface::getHTMLRestrictions().
*
* @return array
* An array of forbidden HTML tags.
*
* @see \Drupal\filter\Plugin\Filter\FilterInterface::getHTMLRestrictions()
*/
protected static function getForbiddenTags($restrictions) {
if ($restrictions === FALSE || !isset($restrictions['forbidden_tags'])) {
return array();
}
else {
return $restrictions['forbidden_tags'];
}
}
/**
* {@inheritdoc}
*/
protected static function needsRemoval($html_tags, $elem) {
// See static::filterXss() about how this class uses blacklisting instead
// of the normal whitelisting.
return !parent::needsRemoval($html_tags, $elem);
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* @file
* Contains \Drupal\editor\EditorXssFilterInterface.
*/
namespace Drupal\editor;
use Drupal\filter\FilterFormatInterface;
/**
* Defines an interface for text editor XSS (Cross-site scripting) filters.
*/
interface EditorXssFilterInterface {
/**
* Filters HTML to prevent XSS attacks when a user edits it in a text editor.
*
* Should filter as minimally as possible, only to remove XSS attack vectors.
*
* Is only called when:
* - loading a non-XSS-safe text editor for a $format that contains a filter
* preventing XSS attacks (a FilterInterface::TYPE_HTML_RESTRICTOR filter):
* if the output is safe, it should also be safe to edit.
* - loading a non-XSS-safe text editor for a $format that doesn't contain a
* filter preventing XSS attacks, but we're switching from a previous text
* format ($original_format is not NULL) that did prevent XSS attacks: if
* the output was previously safe, it should be safe to switch to another
* text format and edit.
*
* @param string $html
* The HTML to be filtered.
* @param \Drupal\filter\FilterFormatInterface $format
* The text format configuration entity. Provides context based upon which
* one may want to adjust the filtering.
* @param \Drupal\filter\FilterFormatInterface|null $original_format
* (optional) The original text format configuration entity (when switching
* text formats/editors). Also provides context based upon which one may
* want to adjust the filtering.
*
* @return string
* The filtered HTML that cannot cause any XSSes anymore.
*/
public static function filterXss($html, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL);
}

View file

@ -0,0 +1,122 @@
<?php
/**
* @file
* Contains \Drupal\editor\Element.
*/
namespace Drupal\editor;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Defines a service for Text Editor's render elements.
*/
class Element {
/**
* The Text Editor plugin manager manager service.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $pluginManager;
/**
* Constructs a new Element object.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
* The Text Editor plugin manager service.
*/
public function __construct(PluginManagerInterface $plugin_manager) {
$this->pluginManager = $plugin_manager;
}
/**
* Additional #pre_render callback for 'text_format' elements.
*/
function preRenderTextFormat(array $element) {
// Allow modules to programmatically enforce no client-side editor by
// setting the #editor property to FALSE.
if (isset($element['#editor']) && !$element['#editor']) {
return $element;
}
// filter_process_format() copies properties to the expanded 'value' child
// element, including the #pre_render property. Skip this text format
// widget, if it contains no 'format'.
if (!isset($element['format'])) {
return $element;
}
$format_ids = array_keys($element['format']['format']['#options']);
// Early-return if no text editor is associated with any of the text formats.
$editors = Editor::loadMultiple($format_ids);
foreach ($editors as $key => $editor) {
$definition = $this->pluginManager->getDefinition($editor->getEditor());
if (!in_array($element['#base_type'], $definition['supported_element_types'])) {
unset($editors[$key]);
}
}
if (count($editors) === 0) {
return $element;
}
// Use a hidden element for a single text format.
$field_id = $element['value']['#id'];
if (!$element['format']['format']['#access']) {
// Use the first (and only) available text format.
$format_id = $format_ids[0];
$element['format']['editor'] = array(
'#type' => 'hidden',
'#name' => $element['format']['format']['#name'],
'#value' => $format_id,
'#attributes' => array(
'data-editor-for' => $field_id,
),
);
}
// Otherwise, attach to text format selector.
else {
$element['format']['format']['#attributes']['class'][] = 'editor';
$element['format']['format']['#attributes']['data-editor-for'] = $field_id;
}
// Hide the text format's filters' guidelines of those text formats that have
// a text editor associated: they're rather useless when using a text editor.
foreach ($editors as $format_id => $editor) {
$element['format']['guidelines'][$format_id]['#access'] = FALSE;
}
// Attach Text Editor module's (this module) library.
$element['#attached']['library'][] = 'editor/drupal.editor';
// Attach attachments for all available editors.
$element['#attached'] = drupal_merge_attached($element['#attached'], $this->pluginManager->getAttachments($format_ids));
// Apply XSS filters when editing content if necessary. Some types of text
// editors cannot guarantee that the end user won't become a victim of XSS.
if (!empty($element['value']['#value'])) {
$original = $element['value']['#value'];
$format = FilterFormat::load($element['format']['format']['#value']);
// Ensure XSS-safety for the current text format/editor.
$filtered = editor_filter_xss($original, $format);
if ($filtered !== FALSE) {
$element['value']['#value'] = $filtered;
}
// Only when the user has access to multiple text formats, we must add data-
// attributes for the original value and change tracking, because they are
// only necessary when the end user can switch between text formats/editors.
if ($element['format']['format']['#access']) {
$element['value']['#attributes']['data-editor-value-is-changed'] = 'false';
$element['value']['#attributes']['data-editor-value-original'] = $original;
}
}
return $element;
}
}

View file

@ -0,0 +1,188 @@
<?php
/**
* @file
* Contains \Drupal\editor\Entity\Editor.
*/
namespace Drupal\editor\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\editor\EditorInterface;
/**
* Defines the configured text editor entity.
*
* @ConfigEntityType(
* id = "editor",
* label = @Translation("Text Editor"),
* entity_keys = {
* "id" = "format"
* },
* config_export = {
* "format",
* "editor",
* "settings",
* "image_upload",
* }
* )
*/
class Editor extends ConfigEntityBase implements EditorInterface {
/**
* The machine name of the text format with which this configured text editor
* is associated.
*
* @var string
*
* @see getFilterFormat()
*/
protected $format;
/**
* The name (plugin ID) of the text editor.
*
* @var string
*/
protected $editor;
/**
* The structured array of text editor plugin-specific settings.
*
* @var array
*/
protected $settings = array();
/**
* The structured array of image upload settings.
*
* @var array
*/
protected $image_upload = array();
/**
* The filter format this text editor is associated with.
*
* @var \Drupal\filter\FilterFormatInterface
*/
protected $filterFormat;
/**
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $editorPluginManager;
/**
* {@inheritdoc}
*/
public function id() {
return $this->format;
}
/**
* {@inheritdoc}
*/
public function __construct(array $values, $entity_type) {
parent::__construct($values, $entity_type);
$plugin = $this->editorPluginManager()->createInstance($this->editor);
$this->settings += $plugin->getDefaultSettings();
}
/**
* {@inheritdoc}
*/
public function label() {
return $this->getFilterFormat()->label();
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
parent::calculateDependencies();
// Create a dependency on the associated FilterFormat.
$this->addDependency('config', $this->getFilterFormat()->getConfigDependencyName());
// @todo use EntityWithPluginCollectionInterface so configuration between
// config entity and dependency on provider is managed automatically.
$definition = $this->editorPluginManager()->createInstance($this->editor)->getPluginDefinition();
$this->addDependency('module', $definition['provider']);
return $this->dependencies;
}
/**
* {@inheritdoc}
*/
public function hasAssociatedFilterFormat() {
return $this->format !== NULL;
}
/**
* {@inheritdoc}
*/
public function getFilterFormat() {
if (!$this->filterFormat) {
$this->filterFormat = \Drupal::entityManager()->getStorage('filter_format')->load($this->format);
}
return $this->filterFormat;
}
/**
* Returns the editor plugin manager.
*
* @return \Drupal\Component\Plugin\PluginManagerInterface
*/
protected function editorPluginManager() {
if (!$this->editorPluginManager) {
$this->editorPluginManager = \Drupal::service('plugin.manager.editor');
}
return $this->editorPluginManager;
}
/**
* {@inheritdoc}
*/
public function getEditor() {
return $this->editor;
}
/**
* {@inheritdoc}
*/
public function setEditor($editor) {
$this->editor = $editor;
return $this;
}
/**
* {@inheritdoc}
*/
public function getSettings() {
return $this->settings;
}
/**
* {@inheritdoc}
*/
public function setSettings(array $settings) {
$this->settings = $settings;
return $this;
}
/**
* {@inheritdoc}
*/
public function getImageUploadSettings() {
return $this->image_upload;
}
/**
* {@inheritdoc}
*/
public function setImageUploadSettings(array $image_upload_settings) {
$this->image_upload = $image_upload_settings;
return $this;
}
}

View file

@ -0,0 +1,243 @@
<?php
/**
* @file
* Contains \Drupal\editor\Form\EditorImageDialog.
*/
namespace Drupal\editor\Form;
use Drupal\Component\Utility\Bytes;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\editor\Ajax\EditorDialogSave;
use Drupal\Core\Ajax\CloseModalDialogCommand;
/**
* Provides an image dialog for text editors.
*/
class EditorImageDialog extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'editor_image_dialog';
}
/**
* {@inheritdoc}
*
* @param \Drupal\filter\Entity\FilterFormat $filter_format
* The filter format for which this dialog corresponds.
*/
public function buildForm(array $form, FormStateInterface $form_state, FilterFormat $filter_format = NULL) {
// The default values are set directly from \Drupal::request()->request,
// provided by the editor plugin opening the dialog.
if (!$image_element = $form_state->get('image_element')) {
$user_input = $form_state->getUserInput();
$image_element = isset($user_input['editor_object']) ? $user_input['editor_object'] : [];
$form_state->set('image_element', $image_element);
}
$form['#tree'] = TRUE;
$form['#attached']['library'][] = 'editor/drupal.editor.dialog';
$form['#prefix'] = '<div id="editor-image-dialog-form">';
$form['#suffix'] = '</div>';
$editor = editor_load($filter_format->id());
// Construct strings to use in the upload validators.
$image_upload = $editor->getImageUploadSettings();
if (!empty($image_upload['dimensions'])) {
$max_dimensions = $image_upload['dimensions']['max_width'] . '×' . $image_upload['dimensions']['max_height'];
}
else {
$max_dimensions = 0;
}
$max_filesize = min(Bytes::toInt($image_upload['max_size']), file_upload_max_size());
$existing_file = isset($image_element['data-entity-uuid']) ? \Drupal::entityManager()->loadEntityByUuid('file', $image_element['data-entity-uuid']) : NULL;
$fid = $existing_file ? $existing_file->id() : NULL;
$form['fid'] = array(
'#title' => $this->t('Image'),
'#type' => 'managed_file',
'#upload_location' => $image_upload['scheme'] . '://' . $image_upload['directory'],
'#default_value' => $fid ? array($fid) : NULL,
'#upload_validators' => array(
'file_validate_extensions' => array('gif png jpg jpeg'),
'file_validate_size' => array($max_filesize),
'file_validate_image_resolution' => array($max_dimensions),
),
'#required' => TRUE,
);
$form['attributes']['src'] = array(
'#title' => $this->t('URL'),
'#type' => 'textfield',
'#default_value' => isset($image_element['src']) ? $image_element['src'] : '',
'#maxlength' => 2048,
'#required' => TRUE,
);
// If the editor has image uploads enabled, show a managed_file form item,
// otherwise show a (file URL) text form item.
if ($image_upload['status']) {
$form['attributes']['src']['#access'] = FALSE;
$form['attributes']['src']['#required'] = FALSE;
}
else {
$form['fid']['#access'] = FALSE;
$form['fid']['#required'] = FALSE;
}
// The alt attribute is *required*, but we allow users to opt-in to empty
// alt attributes for the very rare edge cases where that is valid by
// specifying two double quotes as the alternative text in the dialog.
// However, that *is* stored as an empty alt attribute, so if we're editing
// an existing image (which means the src attribute is set) and its alt
// attribute is empty, then we show that as two double quotes in the dialog.
// @see https://www.drupal.org/node/2307647
$alt = isset($image_element['alt']) ? $image_element['alt'] : '';
if ($alt === '' && !empty($image_element['src'])) {
$alt = '""';
}
$form['attributes']['alt'] = array(
'#title' => $this->t('Alternative text'),
'#placeholder' => $this->t('Short description for the visually impaired'),
'#type' => 'textfield',
'#required' => TRUE,
'#required_error' => $this->t('Alternative text is required.<br />(Only in rare cases should this be left empty. To create empty alternative text, enter <code>""</code> — two double quotes without any content).'),
'#default_value' => $alt,
'#maxlength' => 2048,
);
$form['dimensions'] = array(
'#type' => 'fieldset',
'#title' => $this->t('Image size'),
'#attributes' => array('class' => array(
'container-inline',
'fieldgroup',
'form-composite',
)),
);
$form['dimensions']['width'] = array(
'#title' => $this->t('Width'),
'#title_display' => 'invisible',
'#type' => 'number',
'#default_value' => isset($image_element['width']) ? $image_element['width'] : '',
'#size' => 8,
'#maxlength' => 8,
'#min' => 1,
'#max' => 99999,
'#placeholder' => $this->t('width'),
'#field_suffix' => ' × ',
'#parents' => array('attributes', 'width'),
);
$form['dimensions']['height'] = array(
'#title' => $this->t('Height'),
'#title_display' => 'invisible',
'#type' => 'number',
'#default_value' => isset($image_element['height']) ? $image_element['height'] : '',
'#size' => 8,
'#maxlength' => 8,
'#min' => 1,
'#max' => 99999,
'#placeholder' => $this->t('height'),
'#field_suffix' => $this->t('pixels'),
'#parents' => array('attributes', 'height'),
);
// When Drupal core's filter_align is being used, the text editor may
// offer the ability to change the alignment.
if (isset($image_element['data-align']) && $filter_format->filters('filter_align')->status) {
$form['align'] = array(
'#title' => $this->t('Align'),
'#type' => 'radios',
'#options' => array(
'none' => $this->t('None'),
'left' => $this->t('Left'),
'center' => $this->t('Center'),
'right' => $this->t('Right'),
),
'#default_value' => $image_element['data-align'] === '' ? 'none' : $image_element['data-align'],
'#wrapper_attributes' => array('class' => array('container-inline')),
'#attributes' => array('class' => array('container-inline')),
'#parents' => array('attributes', 'data-align'),
);
}
// When Drupal core's filter_caption is being used, the text editor may
// offer the ability to in-place edit the image's caption: show a toggle.
if (isset($image_element['hasCaption']) && $filter_format->filters('filter_caption')->status) {
$form['caption'] = array(
'#title' => $this->t('Caption'),
'#type' => 'checkbox',
'#default_value' => $image_element['hasCaption'] === 'true',
'#parents' => array('attributes', 'hasCaption'),
);
}
$form['actions'] = array(
'#type' => 'actions',
);
$form['actions']['save_modal'] = array(
'#type' => 'submit',
'#value' => $this->t('Save'),
// No regular submit-handler. This form only works via JavaScript.
'#submit' => array(),
'#ajax' => array(
'callback' => '::submitForm',
'event' => 'click',
),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
// Convert any uploaded files from the FID values to data-entity-uuid
// attributes and set data-entity-type to 'file'.
$fid = $form_state->getValue(array('fid', 0));
if (!empty($fid)) {
$file = file_load($fid);
$file_url = file_create_url($file->getFileUri());
// Transform absolute image URLs to relative image URLs: prevent problems
// on multisite set-ups and prevent mixed content errors.
$file_url = file_url_transform_relative($file_url);
$form_state->setValue(array('attributes', 'src'), $file_url);
$form_state->setValue(array('attributes', 'data-entity-uuid'), $file->uuid());
$form_state->setValue(array('attributes', 'data-entity-type'), 'file');
}
// When the alt attribute is set to two double quotes, transform it to the
// empty string: two double quotes signify "empty alt attribute". See above.
if (trim($form_state->getValue(array('attributes', 'alt'))) === '""') {
$form_state->setValue(array('attributes', 'alt'), '');
}
if ($form_state->getErrors()) {
unset($form['#prefix'], $form['#suffix']);
$form['status_messages'] = [
'#type' => 'status_messages',
'#weight' => -10,
];
$response->addCommand(new HtmlCommand('#editor-image-dialog-form', $form));
}
else {
$response->addCommand(new EditorDialogSave($form_state->getValues()));
$response->addCommand(new CloseModalDialogCommand());
}
return $response;
}
}

View file

@ -0,0 +1,102 @@
<?php
/**
* @file
* Contains \Drupal\editor\Form\EditorLinkDialog.
*/
namespace Drupal\editor\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\HtmlCommand;
use Drupal\editor\Ajax\EditorDialogSave;
use Drupal\Core\Ajax\CloseModalDialogCommand;
/**
* Provides a link dialog for text editors.
*/
class EditorLinkDialog extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'editor_link_dialog';
}
/**
* {@inheritdoc}
*
* @param \Drupal\filter\Entity\FilterFormat $filter_format
* The filter format for which this dialog corresponds.
*/
public function buildForm(array $form, FormStateInterface $form_state, FilterFormat $filter_format = NULL) {
// The default values are set directly from \Drupal::request()->request,
// provided by the editor plugin opening the dialog.
$user_input = $form_state->getUserInput();
$input = isset($user_input['editor_object']) ? $user_input['editor_object'] : array();
$form['#tree'] = TRUE;
$form['#attached']['library'][] = 'editor/drupal.editor.dialog';
$form['#prefix'] = '<div id="editor-link-dialog-form">';
$form['#suffix'] = '</div>';
// Everything under the "attributes" key is merged directly into the
// generated link tag's attributes.
$form['attributes']['href'] = array(
'#title' => $this->t('URL'),
'#type' => 'textfield',
'#default_value' => isset($input['href']) ? $input['href'] : '',
'#maxlength' => 2048,
);
$form['attributes']['target'] = array(
'#title' => $this->t('Open in new window'),
'#type' => 'checkbox',
'#default_value' => !empty($input['target']),
'#return_value' => '_blank',
);
$form['actions'] = array(
'#type' => 'actions',
);
$form['actions']['save_modal'] = array(
'#type' => 'submit',
'#value' => $this->t('Save'),
// No regular submit-handler. This form only works via JavaScript.
'#submit' => array(),
'#ajax' => array(
'callback' => '::submitForm',
'event' => 'click',
),
);
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
$response = new AjaxResponse();
if ($form_state->getErrors()) {
unset($form['#prefix'], $form['#suffix']);
$form['status_messages'] = [
'#type' => 'status_messages',
'#weight' => -10,
];
$response->addCommand(new HtmlCommand('#editor-link-dialog-form', $form));
}
else {
$response->addCommand(new EditorDialogSave($form_state->getValues()));
$response->addCommand(new CloseModalDialogCommand());
}
return $response;
}
}

View file

@ -0,0 +1,79 @@
<?php
/**
* @file
* Contains \Drupal\editor\Plugin\EditorBase.
*/
namespace Drupal\editor\Plugin;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\editor\Entity\Editor;
use Drupal\editor\Plugin\EditorPluginInterface;
/**
* Defines a base class from which other modules providing editors may extend.
*
* This class provides default implementations of the EditorPluginInterface so
* that classes extending this one do not need to implement every method.
*
* Plugins extending this class need to define a plugin definition array through
* annotation. These definition arrays may be altered through
* hook_editor_info_alter(). The definition includes the following keys:
*
* - id: The unique, system-wide identifier of the text editor. Typically named
* the same as the editor library.
* - label: The human-readable name of the text editor, translated.
* - supports_content_filtering: Whether the editor supports "allowed content
* only" filtering.
* - supports_inline_editing: Whether the editor supports the inline editing
* provided by the Edit module.
* - is_xss_safe: Whether this text editor is not vulnerable to XSS attacks.
*
* A complete sample plugin definition should be defined as in this example:
*
* @code
* @Editor(
* id = "myeditor",
* label = @Translation("My Editor"),
* supports_content_filtering = FALSE,
* supports_inline_editing = FALSE,
* is_xss_safe = FALSE
* )
* @endcode
*
* @see \Drupal\editor\Annotation\Editor
* @see \Drupal\editor\Plugin\EditorPluginInterface
* @see \Drupal\editor\Plugin\EditorManager
* @see plugin_api
*/
abstract class EditorBase extends PluginBase implements EditorPluginInterface {
/**
* {@inheritdoc}
*/
public function getDefaultSettings() {
return array();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
return $form;
}
/**
* {@inheritdoc}
*/
public function settingsFormValidate(array $form, FormStateInterface $form_state) {
}
/**
* {@inheritdoc}
*/
public function settingsFormSubmit(array $form, FormStateInterface $form_state) {
}
}

View file

@ -0,0 +1,104 @@
<?php
/**
* @file
* Contains \Drupal\editor\Plugin\EditorManager.
*/
namespace Drupal\editor\Plugin;
use Drupal\Core\Plugin\DefaultPluginManager;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
/**
* Configurable text editor manager.
*
* @see \Drupal\editor\Annotation\Editor
* @see \Drupal\editor\Plugin\EditorPluginInterface
* @see \Drupal\editor\Plugin\EditorBase
* @see plugin_api
*/
class EditorManager extends DefaultPluginManager {
/**
* Constructs an EditorManager object.
*
* @param \Traversable $namespaces
* An object that implements \Traversable which contains the root paths
* keyed by the corresponding namespace to look for plugin implementations.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
* Cache backend instance to use.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler to invoke the alter hook with.
*/
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct('Plugin/Editor', $namespaces, $module_handler, 'Drupal\editor\Plugin\EditorPluginInterface', 'Drupal\editor\Annotation\Editor');
$this->alterInfo('editor_info');
$this->setCacheBackend($cache_backend, 'editor_plugins');
}
/**
* Populates a key-value pair of available text editors.
*
* @return array
* An array of translated text editor labels, keyed by ID.
*/
public function listOptions() {
$options = array();
foreach ($this->getDefinitions() as $key => $definition) {
$options[$key] = $definition['label'];
}
return $options;
}
/**
* Retrieves text editor libraries and JavaScript settings.
*
* @param array $format_ids
* An array of format IDs as returned by array_keys(filter_formats()).
*
* @return array
* An array of attachments, for use with #attached.
*
* @see drupal_process_attached()
*/
public function getAttachments(array $format_ids) {
$attachments = array('library' => array());
$settings = array();
foreach ($format_ids as $format_id) {
$editor = editor_load($format_id);
if (!$editor) {
continue;
}
$plugin = $this->createInstance($editor->getEditor());
$plugin_definition = $plugin->getPluginDefinition();
// Libraries.
$attachments['library'] = array_merge($attachments['library'], $plugin->getLibraries($editor));
// Format-specific JavaScript settings.
$settings['editor']['formats'][$format_id] = array(
'format' => $format_id,
'editor' => $editor->getEditor(),
'editorSettings' => $plugin->getJSSettings($editor),
'editorSupportsContentFiltering' => $plugin_definition['supports_content_filtering'],
'isXssSafe' => $plugin_definition['is_xss_safe'],
);
}
// Allow other modules to alter all JavaScript settings.
$this->moduleHandler->alter('editor_js_settings', $settings);
if (empty($attachments['library']) && empty($settings)) {
return array();
}
$attachments['drupalSettings'] = $settings;
return $attachments;
}
}

View file

@ -0,0 +1,122 @@
<?php
/**
* @file
* Contains \Drupal\editor\Plugin\EditorPluginInterface.
*/
namespace Drupal\editor\Plugin;
use Drupal\Component\Plugin\PluginInspectionInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\editor\Entity\Editor;
/**
* Defines an interface for configurable text editors.
*
* Modules implementing this interface may want to extend the EditorBase class,
* which provides default implementations of each method where appropriate.
*
* @see \Drupal\editor\Annotation\Editor
* @see \Drupal\editor\Plugin\EditorBase
* @see \Drupal\editor\Plugin\EditorManager
* @see plugin_api
*/
interface EditorPluginInterface extends PluginInspectionInterface {
/**
* Returns the default settings for this configurable text editor.
*
* @return array
* An array of settings as they would be stored by a configured text editor
* entity (\Drupal\editor\Entity\Editor).
*/
public function getDefaultSettings();
/**
* Returns a settings form to configure this text editor.
*
* If the editor's behavior depends on extensive options and/or external data,
* then the implementing module can choose to provide a separate, global
* configuration page rather than per-text-format settings. In that case, this
* form should provide a link to the separate settings page.
*
* @param array $form
* An empty form array to be populated with a configuration form, if any.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The state of the entire filter administration form.
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
*
* @return array
* A render array for the settings form.
*/
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor);
/**
* Validates the settings form for an editor.
*
* The contents of the editor settings are located in
* $form_state->getValue(array('editor', 'settings')). Calls to $form_state->setError()
* should reflect this location in the settings form.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function settingsFormValidate(array $form, FormStateInterface $form_state);
/**
* Modifies any values in the form state to prepare them for saving.
*
* Values in $form_state->getValue(array('editor', 'settings')) are saved by
* Editor module in editor_form_filter_admin_format_submit().
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function settingsFormSubmit(array $form, FormStateInterface $form_state);
/**
* Returns JavaScript settings to be attached.
*
* Most text editors use JavaScript to provide a WYSIWYG or toolbar on the
* client-side interface. This method can be used to convert internal settings
* of the text editor into JavaScript variables that will be accessible when
* the text editor is loaded.
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
*
* @return array
* An array of settings that will be added to the page for use by this text
* editor's JavaScript integration.
*
* @see drupal_process_attached()
* @see EditorManager::getAttachments()
*/
public function getJSSettings(Editor $editor);
/**
* Returns libraries to be attached.
*
* Because this is a method, plugins can dynamically choose to attach a
* different library for different configurations, instead of being forced to
* always use the same method.
*
* @param \Drupal\editor\Entity\Editor $editor
* A configured text editor object.
*
* @return array
* An array of libraries that will be added to the page for use by this text
* editor.
*
* @see drupal_process_attached()
* @see EditorManager::getAttachments()
*/
public function getLibraries(Editor $editor);
}

View file

@ -0,0 +1,94 @@
<?php
/**
* @file
* Contains \Drupal\editor\Plugin\Filter\EditorFileReference.
*/
namespace Drupal\editor\Plugin\Filter;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a filter to track images uploaded via a Text Editor.
*
* Passes the text unchanged, but associates the cache tags of referenced files.
*
* @Filter(
* id = "editor_file_reference",
* title = @Translation("Track images uploaded via a Text Editor"),
* description = @Translation("Ensures that the latest versions of images uploaded via a Text Editor are displayed."),
* type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
* )
*/
class EditorFileReference extends FilterBase implements ContainerFactoryPluginInterface {
/**
* An entity manager object.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* Constructs a \Drupal\editor\Plugin\Filter\EditorFileReference object.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin_id for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* An entity manager object.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager) {
$this->entityManager = $entity_manager;
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
static public function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity.manager')
);
}
/**
* {@inheritdoc}
*/
public function process($text, $langcode) {
$result = new FilterProcessResult($text);
if (stristr($text, 'data-entity-type="file"') !== FALSE) {
$dom = Html::load($text);
$xpath = new \DOMXPath($dom);
$processed_uuids = array();
foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid]') as $node) {
$uuid = $node->getAttribute('data-entity-uuid');
// Only process the first occurrence of each file UUID.
if (!isset($processed_uuids[$uuid])) {
$processed_uuids[$uuid] = TRUE;
$file = $this->entityManager->loadEntityByUuid('file', $uuid);
if ($file) {
$result->addCacheTags($file->getCacheTags());
}
}
}
}
return $result;
}
}

View file

@ -0,0 +1,98 @@
<?php
/**
* @file
* Contains \Drupal\editor\Plugin\InPlaceEditor\Editor.
*/
namespace Drupal\editor\Plugin\InPlaceEditor;
use Drupal\Component\Plugin\PluginBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\quickedit\Plugin\InPlaceEditorInterface;
use Drupal\filter\Plugin\FilterInterface;
/**
* Defines the formatted text in-place editor.
*
* @InPlaceEditor(
* id = "editor",
* alternativeTo = {"plain_text"}
* )
*/
class Editor extends PluginBase implements InPlaceEditorInterface {
/**
* {@inheritdoc}
*/
public function isCompatible(FieldItemListInterface $items) {
$field_definition = $items->getFieldDefinition();
// This editor is incompatible with multivalued fields.
if ($field_definition->getFieldStorageDefinition()->getCardinality() != 1) {
return FALSE;
}
// This editor is compatible with formatted ("rich") text fields; but only
// if there is a currently active text format, that text format has an
// associated editor and that editor supports inline editing.
elseif (in_array($field_definition->getType(), array('text', 'text_long', 'text_with_summary'), TRUE)) {
if ($editor = editor_load($items[0]->format)) {
$definition = \Drupal::service('plugin.manager.editor')->getDefinition($editor->getEditor());
if ($definition['supports_inline_editing'] === TRUE) {
return TRUE;
}
}
return FALSE;
}
}
/**
* {@inheritdoc}
*/
function getMetadata(FieldItemListInterface $items) {
$format_id = $items[0]->format;
$metadata['format'] = $format_id;
$metadata['formatHasTransformations'] = $this->textFormatHasTransformationFilters($format_id);
return $metadata;
}
/**
* Returns whether the text format has transformation filters.
*/
protected function textFormatHasTransformationFilters($format_id) {
$format = entity_load('filter_format', $format_id);
return (bool) count(array_intersect(array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE), $format->getFiltertypes()));
}
/**
* {@inheritdoc}
*/
public function getAttachments() {
$user = \Drupal::currentUser();
$user_format_ids = array_keys(filter_formats($user));
$manager = \Drupal::service('plugin.manager.editor');
$definitions = $manager->getDefinitions();
// Filter the current user's formats to those that support inline editing.
$formats = array();
foreach ($user_format_ids as $format_id) {
if ($editor = editor_load($format_id)) {
$editor_id = $editor->getEditor();
if (isset($definitions[$editor_id]['supports_inline_editing']) && $definitions[$editor_id]['supports_inline_editing'] === TRUE) {
$formats[] = $format_id;
}
}
}
// Get the attachments for all text editors that the user might use.
$attachments = $manager->getAttachments($formats);
// Also include editor.module's formatted text editor.
$attachments['library'][] = 'editor/quickedit.inPlaceEditor.formattedText';
return $attachments;
}
}

View file

@ -0,0 +1,173 @@
<?php
/**
* @file
* Contains \Drupal\editor\Tests\EditorAdminTest.
*/
namespace Drupal\editor\Tests;
use Drupal\simpletest\WebTestBase;
/**
* Tests administration of text editors.
*
* @group editor
*/
class EditorAdminTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('filter', 'editor');
/**
* A user with the 'administer filters' permission.
*
* @var \Drupal\user\UserInterface
*/
protected $adminUser;
protected function setUp() {
parent::setUp();
// Add text format.
$filtered_html_format = entity_create('filter_format', array(
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
'filters' => array(),
));
$filtered_html_format->save();
// Create admin user.
$this->adminUser = $this->drupalCreateUser(array('administer filters'));
}
/**
* Tests an existing format without any editors available.
*/
public function testNoEditorAvailable() {
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
// Ensure the form field order is correct.
$roles_pos = strpos($this->getRawContent(), 'Roles');
$editor_pos = strpos($this->getRawContent(), 'Text editor');
$filters_pos = strpos($this->getRawContent(), 'Enabled filters');
$this->assertTrue($roles_pos < $editor_pos && $editor_pos < $filters_pos, '"Text Editor" select appears in the correct location of the text format configuration UI.');
// Verify the <select>.
$select = $this->xpath('//select[@name="editor[editor]"]');
$select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]');
$options = $this->xpath('//select[@name="editor[editor]"]/option');
$this->assertTrue(count($select) === 1, 'The Text Editor select exists.');
$this->assertTrue(count($select_is_disabled) === 1, 'The Text Editor select is disabled.');
$this->assertTrue(count($options) === 1, 'The Text Editor select has only one option.');
$this->assertTrue(((string) $options[0]) === 'None', 'Option 1 in the Text Editor select is "None".');
$this->assertRaw(t('This option is disabled because no modules that provide a text editor are currently enabled.'), 'Description for select present that tells users to install a text editor module.');
}
/**
* Tests adding a text editor to an existing text format.
*/
public function testAddEditorToExistingFormat() {
$this->enableUnicornEditor();
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
$edit = $this->selectUnicornEditor();
// Configure Unicorn Editor's setting to another value.
$edit['editor[settings][ponies_too]'] = FALSE;
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$this->verifyUnicornEditorConfiguration('filtered_html', FALSE);
// Switch back to 'None' and check the Unicorn Editor's settings are gone.
$edit = array(
'editor[editor]' => '',
);
$this->drupalPostAjaxForm(NULL, $edit, 'editor_configure');
$unicorn_setting = $this->xpath('//input[@name="editor[settings][ponies_too]" and @type="checkbox" and @checked]');
$this->assertTrue(count($unicorn_setting) === 0, "Unicorn Editor's settings form is no longer present.");
}
/**
* Tests adding a text editor to a new text format.
*/
public function testAddEditorToNewFormat() {
$this->enableUnicornEditor();
$this->drupalLogin($this->adminUser);
$this->drupalGet('admin/config/content/formats/add');
// Configure the text format name.
$edit = array(
'name' => 'Monocerus',
'format' => 'monocerus',
);
$edit += $this->selectUnicornEditor();
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
$this->verifyUnicornEditorConfiguration($edit['format']);
}
/**
* Enables the unicorn editor.
*/
protected function enableUnicornEditor() {
\Drupal::service('module_installer')->install(array('editor_test'));
}
/**
* Tests and selects the unicorn editor.
*
* @return array
* Returns an edit array containing the values to be posted.
*/
protected function selectUnicornEditor() {
// Verify the <select> when a text editor is available.
$select = $this->xpath('//select[@name="editor[editor]"]');
$select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]');
$options = $this->xpath('//select[@name="editor[editor]"]/option');
$this->assertTrue(count($select) === 1, 'The Text Editor select exists.');
$this->assertTrue(count($select_is_disabled) === 0, 'The Text Editor select is not disabled.');
$this->assertTrue(count($options) === 2, 'The Text Editor select has two options.');
$this->assertTrue(((string) $options[0]) === 'None', 'Option 1 in the Text Editor select is "None".');
$this->assertTrue(((string) $options[1]) === 'Unicorn Editor', 'Option 2 in the Text Editor select is "Unicorn Editor".');
$this->assertTrue(((string) $options[0]['selected']) === 'selected', 'Option 1 ("None") is selected.');
// Ensure the none option is selected.
$this->assertNoRaw(t('This option is disabled because no modules that provide a text editor are currently enabled.'), 'Description for select absent that tells users to install a text editor module.');
// Select the "Unicorn Editor" editor and click the "Configure" button.
$edit = array(
'editor[editor]' => 'unicorn',
);
$this->drupalPostAjaxForm(NULL, $edit, 'editor_configure');
$unicorn_setting = $this->xpath('//input[@name="editor[settings][ponies_too]" and @type="checkbox" and @checked]');
$this->assertTrue(count($unicorn_setting), "Unicorn Editor's settings form is present.");
return $edit;
}
/**
* Verifies unicorn editor configuration.
*
* @param string $format_id
* The format machine name.
* @param bool $ponies_too
* The expected value of the ponies_too setting.
*/
protected function verifyUnicornEditorConfiguration($format_id, $ponies_too = TRUE) {
$editor = editor_load($format_id);
$settings = $editor->getSettings();
$this->assertIdentical($editor->getEditor(), 'unicorn', 'The text editor is configured correctly.');
$this->assertIdentical($settings['ponies_too'], $ponies_too, 'The text editor settings are stored correctly.');
$this->drupalGet('admin/config/content/formats/manage/'. $format_id);
$select = $this->xpath('//select[@name="editor[editor]"]');
$select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]');
$options = $this->xpath('//select[@name="editor[editor]"]/option');
$this->assertTrue(count($select) === 1, 'The Text Editor select exists.');
$this->assertTrue(count($select_is_disabled) === 0, 'The Text Editor select is not disabled.');
$this->assertTrue(count($options) === 2, 'The Text Editor select has two options.');
$this->assertTrue(((string) $options[1]['selected']) === 'selected', 'Option 2 ("Unicorn Editor") is selected.');
}
}

View file

@ -0,0 +1,120 @@
<?php
/**
* @file
* Contains \Drupal\editor\Tests\EditorFileReferenceFilterTest.
*/
namespace Drupal\editor\Tests;
use Drupal\Core\Cache\Cache;
use Drupal\simpletest\KernelTestBase;
use Drupal\filter\FilterPluginCollection;
/**
* Tests Editor module's file reference filter.
*
* @group editor
*/
class EditorFileReferenceFilterTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'filter', 'editor', 'field', 'file', 'user');
/**
* @var \Drupal\filter\Plugin\FilterInterface[]
*/
protected $filters;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$this->installConfig(array('system'));
$this->installEntitySchema('file');
$this->installSchema('file', array('file_usage'));
$manager = $this->container->get('plugin.manager.filter');
$bag = new FilterPluginCollection($manager, array());
$this->filters = $bag->getAll();
}
/**
* Tests the editor file reference filter.
*/
function testEditorFileReferenceFilter() {
$filter = $this->filters['editor_file_reference'];
$test = function($input) use ($filter) {
return $filter->process($input, 'und');
};
file_put_contents('public://llama.jpg', $this->randomMachineName());
$image = entity_create('file', array('uri' => 'public://llama.jpg'));
$image->save();
$id = $image->id();
$uuid = $image->uuid();
$cache_tag = ['file:' . $id];
file_put_contents('public://alpaca.jpg', $this->randomMachineName());
$image_2 = entity_create('file', array('uri' => 'public://alpaca.jpg'));
$image_2->save();
$id_2 = $image_2->id();
$uuid_2 = $image_2->uuid();
$cache_tag_2 = ['file:' . $id_2];
$this->pass('No data-entity-type and no data-entity-uuid attribute.');
$input = '<img src="llama.jpg" />';
$output = $test($input);
$this->assertIdentical($input, $output->getProcessedText());
$this->pass('A non-file data-entity-type attribute value.');
$input = '<img src="llama.jpg" data-entity-type="invalid-entity-type-value" data-entity-uuid="' . $uuid . '" />';
$output = $test($input);
$this->assertIdentical($input, $output->getProcessedText());
$this->pass('One data-entity-uuid attribute.');
$input = '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" />';
$output = $test($input);
$this->assertIdentical($input, $output->getProcessedText());
$this->assertEqual($cache_tag, $output->getCacheTags());
$this->pass('One data-entity-uuid attribute with odd capitalization.');
$input = '<img src="llama.jpg" data-entity-type="file" DATA-entity-UUID = "' . $uuid . '" />';
$output = $test($input);
$this->assertIdentical($input, $output->getProcessedText());
$this->assertEqual($cache_tag, $output->getCacheTags());
$this->pass('One data-entity-uuid attribute on a non-image tag.');
$input = '<video src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" />';
$output = $test($input);
$this->assertIdentical($input, $output->getProcessedText());
$this->assertEqual($cache_tag, $output->getCacheTags());
$this->pass('One data-entity-uuid attribute with an invalid value.');
$input = '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="invalid-' . $uuid . '" />';
$output = $test($input);
$this->assertIdentical($input, $output->getProcessedText());
$this->assertEqual(array(), $output->getCacheTags());
$this->pass('Two different data-entity-uuid attributes.');
$input = '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" />';
$input .= '<img src="alpaca.jpg" data-entity-type="file" data-entity-uuid="' . $uuid_2 . '" />';
$output = $test($input);
$this->assertIdentical($input, $output->getProcessedText());
$this->assertEqual(Cache::mergeTags($cache_tag, $cache_tag_2), $output->getCacheTags());
$this->pass('Two identical data-entity-uuid attributes.');
$input = '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" />';
$input .= '<img src="llama.jpg" data-entity-type="file" data-entity-uuid="' . $uuid . '" />';
$output = $test($input);
$this->assertIdentical($input, $output->getProcessedText());
$this->assertEqual($cache_tag, $output->getCacheTags());
}
}

View file

@ -0,0 +1,134 @@
<?php
/**
* @file
* Contains \Drupal\editor\Tests\EditorFileUsageTest.
*/
namespace Drupal\editor\Tests;
use Drupal\system\Tests\Entity\EntityUnitTestBase;
/**
* Tests tracking of file usage by the Text Editor module.
*
* @group editor
*/
class EditorFileUsageTest extends EntityUnitTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('editor', 'editor_test', 'node', 'file');
protected function setUp() {
parent::setUp();
$this->installEntitySchema('file');
$this->installSchema('node', array('node_access'));
$this->installSchema('file', array('file_usage'));
$this->installConfig(['node']);
// Add text formats.
$filtered_html_format = entity_create('filter_format', array(
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
'filters' => array(),
));
$filtered_html_format->save();
// Set up text editor.
$editor = entity_create('editor', array(
'format' => 'filtered_html',
'editor' => 'unicorn',
));
$editor->save();
// Create a node type for testing.
$type = entity_create('node_type', array('type' => 'page', 'name' => 'page'));
$type->save();
node_add_body_field($type);
}
/**
* Tests the configurable text editor manager.
*/
public function testEditorEntityHooks() {
$image = entity_create('file');
$image->setFileUri('core/misc/druplicon.png');
$image->setFilename(drupal_basename($image->getFileUri()));
$image->save();
$file_usage = $this->container->get('file.usage');
$this->assertIdentical(array(), $file_usage->listUsage($image), 'The image has zero usages.');
$body_value = '<p>Hello, world!</p><img src="awesome-llama.jpg" data-entity-type="file" data-entity-uuid="' . $image->uuid() . '" />';
// Test handling of an invalid data-entity-uuid attribute.
$body_value .= '<img src="awesome-llama.jpg" data-entity-type="file" data-entity-uuid="invalid-entity-uuid-value" />';
// Test handling of an invalid data-entity-type attribute.
$body_value .= '<img src="awesome-llama.jpg" data-entity-type="invalid-entity-type-value" data-entity-uuid="' . $image->uuid() . '" />';
// Test handling of a non-existing UUID.
$body_value .= '<img src="awesome-llama.jpg" data-entity-type="file" data-entity-uuid="30aac704-ba2c-40fc-b609-9ed121aa90f4" />';
// Test editor_entity_insert(): increment.
$this->createUser();
$node = entity_create('node', array(
'type' => 'page',
'title' => 'test',
'body' => array(
'value' => $body_value,
'format' => 'filtered_html',
),
'uid' => 1,
));
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '1'))), $file_usage->listUsage($image), 'The image has 1 usage.');
// Test editor_entity_update(): increment, twice, by creating new revisions.
$node->setNewRevision(TRUE);
$node->save();
$second_revision_id = $node->getRevisionId();
$node->setNewRevision(TRUE);
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.');
// Test hook_entity_update(): decrement, by modifying the last revision:
// remove the data-entity-type attribute from the body field.
$body = $node->get('body')->first()->get('value');
$original_value = $body->getValue();
$new_value = str_replace('data-entity-type', 'data-entity-type-modified', $original_value);
$body->setValue($new_value);
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.');
// Test editor_entity_update(): increment again by creating a new revision:
// read the data- attributes to the body field.
$node->setNewRevision(TRUE);
$node->get('body')->first()->get('value')->setValue($original_value);
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.');
// Test hook_entity_update(): decrement, by modifying the last revision:
// remove the data-entity-uuid attribute from the body field.
$body = $node->get('body')->first()->get('value');
$new_value = str_replace('data-entity-uuid', 'data-entity-uuid-modified', $original_value);
$body->setValue($new_value);
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.');
// Test hook_entity_update(): increment, by modifying the last revision:
// read the data- attributes to the body field.
$node->get('body')->first()->get('value')->setValue($original_value);
$node->save();
$this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), $file_usage->listUsage($image), 'The image has 3 usages.');
// Test editor_entity_revision_delete(): decrement, by deleting a revision.
entity_revision_delete('node', $second_revision_id);
$this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), $file_usage->listUsage($image), 'The image has 2 usages.');
// Test editor_entity_delete().
$node->delete();
$this->assertIdentical(array(), $file_usage->listUsage($image), 'The image has zero usages again.');
}
}

View file

@ -0,0 +1,289 @@
<?php
/**
* @file
* Contains \Drupal\editor\Tests\EditorLoadingTest.
*/
namespace Drupal\editor\Tests;
use Drupal\Core\Entity\Entity;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\simpletest\WebTestBase;
/**
* Tests loading of text editors.
*
* @group editor
*/
class EditorLoadingTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('filter', 'editor', 'editor_test', 'node');
/**
* An untrusted user, with access to the 'plain_text' format.
*
* @var \Drupal\user\UserInterface
*/
protected $untrustedUser;
/**
* A normal user with additional access to the 'filtered_html' format.
*
* @var \Drupal\user\UserInterface
*/
protected $normalUser;
/**
* A privileged user with additional access to the 'full_html' format.
*
* @var \Drupal\user\UserInterface
*/
protected $privilegedUser;
protected function setUp() {
parent::setUp();
// Let there be T-rex.
\Drupal::state()->set('editor_test_give_me_a_trex_thanks', TRUE);
\Drupal::service('plugin.manager.editor')->clearCachedDefinitions();
// Add text formats.
$filtered_html_format = entity_create('filter_format', array(
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
'filters' => array(),
));
$filtered_html_format->save();
$full_html_format = entity_create('filter_format', array(
'format' => 'full_html',
'name' => 'Full HTML',
'weight' => 1,
'filters' => array(),
));
$full_html_format->save();
// Create article node type.
$this->drupalCreateContentType(array(
'type' => 'article',
'name' => 'Article',
));
// Create page node type, but remove the body.
$this->drupalCreateContentType(array(
'type' => 'page',
'name' => 'Page',
));
$body = FieldConfig::loadByName('node', 'page', 'body');
$body->delete();
// Create a formatted text field, which uses an <input type="text">.
FieldStorageConfig::create(array(
'field_name' => 'field_text',
'entity_type' => 'node',
'type' => 'text',
))->save();
FieldConfig::create(array(
'field_name' => 'field_text',
'entity_type' => 'node',
'label' => 'Textfield',
'bundle' => 'page',
))->save();
entity_get_form_display('node', 'page', 'default')
->setComponent('field_text')
->save();
// Create 3 users, each with access to different text formats.
$this->untrustedUser = $this->drupalCreateUser(array('create article content', 'edit any article content'));
$this->normalUser = $this->drupalCreateUser(array('create article content', 'edit any article content', 'use text format filtered_html'));
$this->privilegedUser = $this->drupalCreateUser(array('create article content', 'edit any article content', 'create page content', 'edit any page content', 'use text format filtered_html', 'use text format full_html'));
}
/**
* Tests loading of text editors.
*/
public function testLoading() {
// Only associate a text editor with the "Full HTML" text format.
$editor = entity_create('editor', array(
'format' => 'full_html',
'editor' => 'unicorn',
'image_upload' => array(
'status' => FALSE,
'scheme' => file_default_scheme(),
'directory' => 'inline-images',
'max_size' => '',
'max_dimensions' => array('width' => '', 'height' => ''),
)
));
$editor->save();
// The normal user:
// - has access to 2 text formats;
// - doesn't have access to the full_html text format, so: no text editor.
$this->drupalLogin($this->normalUser);
$this->drupalGet('node/add/article');
list( , $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
$this->assertFalse($editor_settings_present, 'No Text Editor module settings.');
$this->assertFalse($editor_js_present, 'No Text Editor JavaScript.');
$this->assertTrue(count($body) === 1, 'A body field exists.');
$this->assertTrue(count($format_selector) === 0, 'No text format selector exists on the page because the user only has access to a single format.');
$this->drupalLogout($this->normalUser);
// The privileged user:
// - has access to 2 text formats (and the fallback format);
// - does have access to the full_html text format, so: Unicorn text editor.
$this->drupalLogin($this->privilegedUser);
$this->drupalGet('node/add/article');
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
$expected = array('formats' => array('full_html' => array(
'format' => 'full_html',
'editor' => 'unicorn',
'editorSettings' => array('ponyModeEnabled' => TRUE),
'editorSupportsContentFiltering' => TRUE,
'isXssSafe' => FALSE,
)));
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
$this->assertIdentical($expected, $settings['editor'], "Text Editor module's JavaScript settings on the page are correct.");
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
$this->assertTrue(count($body) === 1, 'A body field exists.');
$this->assertTrue(count($format_selector) === 1, 'A single text format selector exists on the page.');
$specific_format_selector = $this->xpath('//select[contains(@class, "filter-list") and @data-editor-for="edit-body-0-value"]');
$this->assertTrue(count($specific_format_selector) === 1, 'A single text format selector exists on the page and has a "data-editor-for" attribute with the correct value.');
// Load the editor image dialog form and make sure it does not fatal.
$this->drupalGet('editor/dialog/image/full_html');
$this->assertResponse(200);
$this->drupalLogout($this->privilegedUser);
// Also associate a text editor with the "Plain Text" text format.
$editor = entity_create('editor', array(
'format' => 'plain_text',
'editor' => 'unicorn',
));
$editor->save();
// The untrusted user:
// - has access to 1 text format (plain_text);
// - has access to the plain_text text format, so: Unicorn text editor.
$this->drupalLogin($this->untrustedUser);
$this->drupalGet('node/add/article');
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
$expected = array('formats' => array('plain_text' => array(
'format' => 'plain_text',
'editor' => 'unicorn',
'editorSettings' => array('ponyModeEnabled' => TRUE),
'editorSupportsContentFiltering' => TRUE,
'isXssSafe' => FALSE,
)));
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
$this->assertIdentical($expected, $settings['editor'], "Text Editor module's JavaScript settings on the page are correct.");
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
$this->assertTrue(count($body) === 1, 'A body field exists.');
$this->assertTrue(count($format_selector) === 0, 'No text format selector exists on the page.');
$hidden_input = $this->xpath('//input[@type="hidden" and @value="plain_text" and @data-editor-for="edit-body-0-value"]');
$this->assertTrue(count($hidden_input) === 1, 'A single text format hidden input exists on the page and has a "data-editor-for" attribute with the correct value.');
// Create an "article" node that uses the full_html text format, then try
// to let the untrusted user edit it.
$this->drupalCreateNode(array(
'type' => 'article',
'body' => array(
array('value' => $this->randomMachineName(32), 'format' => 'full_html')
),
));
// The untrusted user tries to edit content that is written in a text format
// that (s)he is not allowed to use. The editor is still loaded. CKEditor,
// for example, supports being loaded in a disabled state.
$this->drupalGet('node/1/edit');
list( , $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
$this->assertTrue($editor_settings_present, 'Text Editor module settings.');
$this->assertTrue($editor_js_present, 'Text Editor JavaScript.');
$this->assertTrue(count($body) === 1, 'A body field exists.');
$this->assertFieldByXPath('//textarea[@id="edit-body-0-value" and @disabled="disabled"]', t('This field has been disabled because you do not have sufficient permissions to edit it.'), 'Text format access denied message found.');
$this->assertTrue(count($format_selector) === 0, 'No text format selector exists on the page.');
$hidden_input = $this->xpath('//input[@type="hidden" and contains(@class, "editor")]');
$this->assertTrue(count($hidden_input) === 0, 'A single text format hidden input does not exist on the page.');
}
/**
* Test supported element types.
*/
public function testSupportedElementTypes() {
// Associate the unicorn text editor with the "Full HTML" text format.
$editor = entity_create('editor', array(
'format' => 'full_html',
'editor' => 'unicorn',
'image_upload' => array(
'status' => FALSE,
'scheme' => file_default_scheme(),
'directory' => 'inline-images',
'max_size' => '',
'max_dimensions' => array('width' => '', 'height' => ''),
)
));
$editor->save();
// Create an "page" node that uses the full_html text format.
$this->drupalCreateNode(array(
'type' => 'page',
'field_text' => array(
array('value' => $this->randomMachineName(32), 'format' => 'full_html')
),
));
// Assert the unicorn editor works with textfields.
$this->drupalLogin($this->privilegedUser);
$this->drupalGet('node/1/edit');
list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
$this->assertTrue(count($field) === 1, 'A text field exists.');
$this->assertTrue(count($format_selector) === 1, 'A single text format selector exists on the page.');
$specific_format_selector = $this->xpath('//select[contains(@class, "filter-list") and contains(@class, "editor") and @data-editor-for="edit-field-text-0-value"]');
$this->assertTrue(count($specific_format_selector) === 1, 'A single text format selector exists on the page and has the "editor" class and a "data-editor-for" attribute with the correct value.');
// Associate the trex text editor with the "Full HTML" text format.
$editor->delete();
entity_create('editor', array(
'format' => 'full_html',
'editor' => 'trex',
))->save();
$this->drupalGet('node/1/edit');
list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
$this->assertFalse($editor_settings_present, "Text Editor module's JavaScript settings are not on the page.");
$this->assertFalse($editor_js_present, 'Text Editor JavaScript is not present.');
$this->assertTrue(count($field) === 1, 'A text field exists.');
$this->assertTrue(count($format_selector) === 1, 'A single text format selector exists on the page.');
$specific_format_selector = $this->xpath('//select[contains(@class, "filter-list") and contains(@class, "editor") and @data-editor-for="edit-field-text-0-value"]');
$this->assertFalse(count($specific_format_selector) === 1, 'A single text format selector exists on the page and has the "editor" class and a "data-editor-for" attribute with the correct value.');
}
protected function getThingsToCheck($field_name, $type = 'textarea') {
$settings = $this->getDrupalSettings();
return array(
// JavaScript settings.
$settings,
// Editor.module's JS settings present.
isset($settings['editor']),
// Editor.module's JS present.
strpos($this->getRawContent(), drupal_get_path('module', 'editor') . '/js/editor.js') !== FALSE,
// Body field.
$this->xpath('//' . $type . '[@id="edit-' . $field_name . '-0-value"]'),
// Format selector.
$this->xpath('//select[contains(@class, "filter-list")]'),
);
}
}

View file

@ -0,0 +1,116 @@
<?php
/**
* @file
* Contains \Drupal\editor\Tests\EditorManagerTest.
*/
namespace Drupal\editor\Tests;
use Drupal\simpletest\KernelTestBase;
use Drupal\editor\Plugin\EditorManager;
/**
* Tests detection of text editors and correct generation of attachments.
*
* @group editor
*/
class EditorManagerTest extends KernelTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('system', 'user', 'filter', 'editor');
/**
* The manager for text editor plugins.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $editorManager;
protected function setUp() {
parent::setUp();
// Install the Filter module.
$this->installSchema('system', 'url_alias');
// Add text formats.
$filtered_html_format = entity_create('filter_format', array(
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
'filters' => array(),
));
$filtered_html_format->save();
$full_html_format = entity_create('filter_format', array(
'format' => 'full_html',
'name' => 'Full HTML',
'weight' => 1,
'filters' => array(),
));
$full_html_format->save();
}
/**
* Tests the configurable text editor manager.
*/
public function testManager() {
$this->editorManager = $this->container->get('plugin.manager.editor');
// Case 1: no text editor available:
// - listOptions() should return an empty list of options
// - getAttachments() should return an empty #attachments array (and not
// a JS settings structure that is empty)
$this->assertIdentical(array(), $this->editorManager->listOptions(), 'When no text editor is enabled, the manager works correctly.');
$this->assertIdentical(array(), $this->editorManager->getAttachments(array()), 'No attachments when no text editor is enabled and retrieving attachments for zero text formats.');
$this->assertIdentical(array(), $this->editorManager->getAttachments(array('filtered_html', 'full_html')), 'No attachments when no text editor is enabled and retrieving attachments for multiple text formats.');
// Enable the Text Editor Test module, which has the Unicorn Editor and
// clear the editor manager's cache so it is picked up.
$this->enableModules(array('editor_test'));
$this->editorManager = $this->container->get('plugin.manager.editor');
$this->editorManager->clearCachedDefinitions();
// Case 2: a text editor available.
$this->assertIdentical('Unicorn Editor', (string) $this->editorManager->listOptions()['unicorn'], 'When some text editor is enabled, the manager works correctly.');
// Case 3: a text editor available & associated (but associated only with
// the 'Full HTML' text format).
$unicorn_plugin = $this->editorManager->createInstance('unicorn');
$editor = entity_create('editor', array(
'format' => 'full_html',
'editor' => 'unicorn',
));
$editor->save();
$this->assertIdentical(array(), $this->editorManager->getAttachments(array()), 'No attachments when one text editor is enabled and retrieving attachments for zero text formats.');
$expected = array(
'library' => array(
0 => 'editor_test/unicorn',
),
'drupalSettings' => [
'editor' => [
'formats' => [
'full_html' => [
'format' => 'full_html',
'editor' => 'unicorn',
'editorSettings' => $unicorn_plugin->getJSSettings($editor),
'editorSupportsContentFiltering' => TRUE,
'isXssSafe' => FALSE,
],
],
],
],
);
$this->assertIdentical($expected, $this->editorManager->getAttachments(array('filtered_html', 'full_html')), 'Correct attachments when one text editor is enabled and retrieving attachments for multiple text formats.');
// Case 4: a text editor available associated, but now with its JS settings
// being altered via hook_editor_js_settings_alter().
\Drupal::state()->set('editor_test_js_settings_alter_enabled', TRUE);
$expected['drupalSettings']['editor']['formats']['full_html']['editorSettings']['ponyModeEnabled'] = FALSE;
$this->assertIdentical($expected, $this->editorManager->getAttachments(array('filtered_html', 'full_html')), 'hook_editor_js_settings_alter() works correctly.');
}
}

View file

@ -0,0 +1,439 @@
<?php
/**
* @file
* Contains \Drupal\editor\Tests\EditorSecurityTest.
*/
namespace Drupal\editor\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\simpletest\WebTestBase;
use Drupal\Component\Utility\SafeMarkup;
/**
* Tests XSS protection for content creators when using text editors.
*
* @group editor
*/
class EditorSecurityTest extends WebTestBase {
/**
* The sample content to use in all tests.
*
* @var string
*/
protected static $sampleContent = '<p style="color: red">Hello, Dumbo Octopus!</p><script>alert(0)</script><embed type="image/svg+xml" src="image.svg" />';
/**
* The secured sample content to use in most tests.
*
* @var string
*/
protected static $sampleContentSecured = '<p>Hello, Dumbo Octopus!</p>alert(0)';
/**
* The secured sample content to use in tests when the <embed> tag is allowed.
*
* @var string
*/
protected static $sampleContentSecuredEmbedAllowed = '<p>Hello, Dumbo Octopus!</p>alert(0)<embed type="image/svg+xml" src="image.svg" />';
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('filter', 'editor', 'editor_test', 'node');
/**
* User with access to Restricted HTML text format without text editor.
*
* @var \Drupal\user\UserInterface
*/
protected $untrustedUser;
/**
* User with access to Restricted HTML text format with text editor.
*
* @var \Drupal\user\UserInterface
*/
protected $normalUser;
/**
* User with access to Restricted HTML text format, dangerous tags allowed
* with text editor.
*
* @var \Drupal\user\UserInterface
*/
protected $trustedUser;
/**
* User with access to all text formats and text editors.
*
* @var \Drupal\user\UserInterface
*/
protected $privilegedUser;
protected function setUp() {
parent::setUp();
// Create 5 text formats, to cover all potential use cases:
// 1. restricted_without_editor (untrusted: anonymous)
// 2. restricted_with_editor (normal: authenticated)
// 3. restricted_plus_dangerous_tag_with_editor (privileged: trusted)
// 4. unrestricted_without_editor (privileged: admin)
// 5. unrestricted_with_editor (privileged: admin)
// With text formats 2, 3 and 5, we also associate a text editor that does
// not guarantee XSS safety. "restricted" means the text format has XSS
// filters on output, "unrestricted" means the opposite.
$format = entity_create('filter_format', array(
'format' => 'restricted_without_editor',
'name' => 'Restricted HTML, without text editor',
'weight' => 0,
'filters' => array(
// A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
'filter_html' => array(
'status' => 1,
'settings' => array(
'allowed_html' => '<h4> <h5> <h6> <p> <br> <strong> <a>',
)
),
),
));
$format->save();
$format = entity_create('filter_format', array(
'format' => 'restricted_with_editor',
'name' => 'Restricted HTML, with text editor',
'weight' => 1,
'filters' => array(
// A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
'filter_html' => array(
'status' => 1,
'settings' => array(
'allowed_html' => '<h4> <h5> <h6> <p> <br> <strong> <a>',
)
),
),
));
$format->save();
$editor = entity_create('editor', array(
'format' => 'restricted_with_editor',
'editor' => 'unicorn',
));
$editor->save();
$format = entity_create('filter_format', array(
'format' => 'restricted_plus_dangerous_tag_with_editor',
'name' => 'Restricted HTML, dangerous tag allowed, with text editor',
'weight' => 1,
'filters' => array(
// A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
'filter_html' => array(
'status' => 1,
'settings' => array(
'allowed_html' => '<h4> <h5> <h6> <p> <br> <strong> <a> <embed>',
)
),
),
));
$format->save();
$editor = entity_create('editor', array(
'format' => 'restricted_plus_dangerous_tag_with_editor',
'editor' => 'unicorn',
));
$editor->save();
$format = entity_create('filter_format', array(
'format' => 'unrestricted_without_editor',
'name' => 'Unrestricted HTML, without text editor',
'weight' => 0,
'filters' => array(),
));
$format->save();
$format = entity_create('filter_format', array(
'format' => 'unrestricted_with_editor',
'name' => 'Unrestricted HTML, with text editor',
'weight' => 1,
'filters' => array(),
));
$format->save();
$editor = entity_create('editor', array(
'format' => 'unrestricted_with_editor',
'editor' => 'unicorn',
));
$editor->save();
// Create node type.
$this->drupalCreateContentType(array(
'type' => 'article',
'name' => 'Article',
));
// Create 4 users, each with access to different text formats/editors:
// - "untrusted": restricted_without_editor
// - "normal": restricted_with_editor,
// - "trusted": restricted_plus_dangerous_tag_with_editor
// - "privileged": restricted_without_editor, restricted_with_editor,
// restricted_plus_dangerous_tag_with_editor,
// unrestricted_without_editor and unrestricted_with_editor
$this->untrustedUser = $this->drupalCreateUser(array(
'create article content',
'edit any article content',
'use text format restricted_without_editor',
));
$this->normalUser = $this->drupalCreateUser(array(
'create article content',
'edit any article content',
'use text format restricted_with_editor',
));
$this->trustedUser = $this->drupalCreateUser(array(
'create article content',
'edit any article content',
'use text format restricted_plus_dangerous_tag_with_editor',
));
$this->privilegedUser = $this->drupalCreateUser(array(
'create article content',
'edit any article content',
'use text format restricted_without_editor',
'use text format restricted_with_editor',
'use text format restricted_plus_dangerous_tag_with_editor',
'use text format unrestricted_without_editor',
'use text format unrestricted_with_editor',
));
// Create an "article" node for each possible text format, with the same
// sample content, to do our tests on.
$samples = array(
array('author' => $this->untrustedUser->id(), 'format' => 'restricted_without_editor'),
array('author' => $this->normalUser->id(), 'format' => 'restricted_with_editor'),
array('author' => $this->trustedUser->id(), 'format' => 'restricted_plus_dangerous_tag_with_editor'),
array('author' => $this->privilegedUser->id(), 'format' => 'unrestricted_without_editor'),
array('author' => $this->privilegedUser->id(), 'format' => 'unrestricted_with_editor'),
);
foreach ($samples as $sample) {
$this->drupalCreateNode(array(
'type' => 'article',
'body' => array(
array('value' => self::$sampleContent, 'format' => $sample['format'])
),
'uid' => $sample['author']
));
}
}
/**
* Tests initial security: is the user safe without switching text formats?
*
* Tests 8 scenarios. Tests only with a text editor that is not XSS-safe.
*/
function testInitialSecurity() {
$expected = array(
array(
'node_id' => 1,
'format' => 'restricted_without_editor',
// No text editor => no XSS filtering.
'value' => self::$sampleContent,
'users' => array(
$this->untrustedUser,
$this->privilegedUser,
),
),
array(
'node_id' => 2,
'format' => 'restricted_with_editor',
// Text editor => XSS filtering.
'value' => self::$sampleContentSecured,
'users' => array(
$this->normalUser,
$this->privilegedUser,
),
),
array(
'node_id' => 3,
'format' => 'restricted_plus_dangerous_tag_with_editor',
// Text editor => XSS filtering.
'value' => self::$sampleContentSecuredEmbedAllowed,
'users' => array(
$this->trustedUser,
$this->privilegedUser,
),
),
array(
'node_id' => 4,
'format' => 'unrestricted_without_editor',
// No text editor => no XSS filtering.
'value' => self::$sampleContent,
'users' => array(
$this->privilegedUser,
),
),
array(
'node_id' => 5,
'format' => 'unrestricted_with_editor',
// Text editor, no security filter => no XSS filtering.
'value' => self::$sampleContent,
'users' => array(
$this->privilegedUser,
),
),
);
// Log in as each user that may edit the content, and assert the value.
foreach ($expected as $case) {
foreach ($case['users'] as $account) {
$this->pass(format_string('Scenario: sample %sample_id, %format.', array(
'%sample_id' => $case['node_id'],
'%format' => $case['format'],
)));
$this->drupalLogin($account);
$this->drupalGet('node/' . $case['node_id'] . '/edit');
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
$this->assertIdentical($case['value'], (string) $dom_node[0], 'The value was correctly filtered for XSS attack vectors.');
}
}
}
/**
* Tests administrator security: is the user safe when switching text formats?
*
* Tests 24 scenarios. Tests only with a text editor that is not XSS-safe.
*
* When changing from a more restrictive text format with a text editor (or a
* text format without a text editor) to a less restrictive text format, it is
* possible that a malicious user could trigger an XSS.
*
* E.g. when switching a piece of text that uses the Restricted HTML text
* format and contains a <script> tag to the Full HTML text format, the
* <script> tag would be executed. Unless we apply appropriate filtering.
*/
function testSwitchingSecurity() {
$expected = array(
array(
'node_id' => 1,
'value' => self::$sampleContent, // No text editor => no XSS filtering.
'format' => 'restricted_without_editor',
'switch_to' => array(
'restricted_with_editor' => self::$sampleContentSecured,
// Intersection of restrictions => most strict XSS filtering.
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
// No text editor => no XSS filtering.
'unrestricted_without_editor' => FALSE,
'unrestricted_with_editor' => self::$sampleContentSecured,
),
),
array(
'node_id' => 2,
'value' => self::$sampleContentSecured, // Text editor => XSS filtering.
'format' => 'restricted_with_editor',
'switch_to' => array(
// No text editor => no XSS filtering.
'restricted_without_editor' => FALSE,
// Intersection of restrictions => most strict XSS filtering.
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
// No text editor => no XSS filtering.
'unrestricted_without_editor' => FALSE,
'unrestricted_with_editor' => self::$sampleContentSecured,
),
),
array(
'node_id' => 3,
'value' => self::$sampleContentSecuredEmbedAllowed, // Text editor => XSS filtering.
'format' => 'restricted_plus_dangerous_tag_with_editor',
'switch_to' => array(
// No text editor => no XSS filtering.
'restricted_without_editor' => FALSE,
// Intersection of restrictions => most strict XSS filtering.
'restricted_with_editor' => self::$sampleContentSecured,
// No text editor => no XSS filtering.
'unrestricted_without_editor' => FALSE,
// Intersection of restrictions => most strict XSS filtering.
'unrestricted_with_editor' => self::$sampleContentSecured,
),
),
array(
'node_id' => 4,
'value' => self::$sampleContent, // No text editor => no XSS filtering.
'format' => 'unrestricted_without_editor',
'switch_to' => array(
// No text editor => no XSS filtering.
'restricted_without_editor' => FALSE,
'restricted_with_editor' => self::$sampleContentSecured,
// Intersection of restrictions => most strict XSS filtering.
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
// From no editor, no security filters, to editor, still no security
// filters: resulting content when viewed was already vulnerable, so
// it must be intentional.
'unrestricted_with_editor' => FALSE,
),
),
array(
'node_id' => 5,
'value' => self::$sampleContentSecured, // Text editor => XSS filtering.
'format' => 'unrestricted_with_editor',
'switch_to' => array(
// From editor, no security filters to security filters, no editor: no
// risk.
'restricted_without_editor' => FALSE,
'restricted_with_editor' => self::$sampleContentSecured,
// Intersection of restrictions => most strict XSS filtering.
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
// From no editor, no security filters, to editor, still no security
// filters: resulting content when viewed was already vulnerable, so
// it must be intentional.
'unrestricted_without_editor' => FALSE,
),
),
);
// Log in as the privileged user, and for every sample, do the following:
// - switch to every other text format/editor
// - assert the XSS-filtered values that we get from the server
$value_original_attribute = SafeMarkup::checkPlain(self::$sampleContent);
$this->drupalLogin($this->privilegedUser);
foreach ($expected as $case) {
$this->drupalGet('node/' . $case['node_id'] . '/edit');
// Verify data- attributes.
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
$this->assertIdentical(self::$sampleContent, (string) $dom_node[0]['data-editor-value-original'], 'The data-editor-value-original attribute is correctly set.');
$this->assertIdentical('false', (string) $dom_node[0]['data-editor-value-is-changed'], 'The data-editor-value-is-changed attribute is correctly set.');
// Switch to every other text format/editor and verify the results.
foreach ($case['switch_to'] as $format => $expected_filtered_value) {
$this->pass(format_string('Scenario: sample %sample_id, switch from %original_format to %format.', array(
'%sample_id' => $case['node_id'],
'%original_format' => $case['format'],
'%format' => $format,
)));
$post = array(
'value' => self::$sampleContent,
'original_format_id' => $case['format'],
);
$response = $this->drupalPostWithFormat('editor/filter_xss/' . $format, 'json', $post);
$this->assertResponse(200);
$json = Json::decode($response);
$this->assertIdentical($json, $expected_filtered_value, 'The value was correctly filtered for XSS attack vectors.');
}
}
}
/**
* Tests the standard text editor XSS filter being overridden.
*/
function testEditorXssFilterOverride() {
// First: the Standard text editor XSS filter.
$this->drupalLogin($this->normalUser);
$this->drupalGet('node/2/edit');
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
$this->assertIdentical(self::$sampleContentSecured, (string) $dom_node[0], 'The value was filtered by the Standard text editor XSS filter.');
// Enable editor_test.module's hook_editor_xss_filter_alter() implementation
// to alter the text editor XSS filter class being used.
\Drupal::state()->set('editor_test_editor_xss_filter_alter_enabled', TRUE);
// First: the Insecure text editor XSS filter.
$this->drupalGet('node/2/edit');
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
$this->assertIdentical(self::$sampleContent, (string) $dom_node[0], 'The value was filtered by the Insecure text editor XSS filter.');
}
}

View file

@ -0,0 +1,117 @@
<?php
/**
* @file
* Contains \Drupal\editor\Tests\QuickEditIntegrationLoadingTest.
*/
namespace Drupal\editor\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
use Drupal\simpletest\WebTestBase;
/**
* Tests Quick Edit module integration endpoints.
*
* @group editor
*/
class QuickEditIntegrationLoadingTest extends WebTestBase {
/**
* Modules to enable.
*
* @var array
*/
public static $modules = array('quickedit', 'filter', 'node', 'editor');
/**
* The basic permissions necessary to view content and use in-place editing.
*
* @var array
*/
protected static $basicPermissions = array('access content', 'create article content', 'use text format filtered_html', 'access contextual links');
protected function setUp() {
parent::setUp();
// Create a text format.
$filtered_html_format = entity_create('filter_format', array(
'format' => 'filtered_html',
'name' => 'Filtered HTML',
'weight' => 0,
'filters' => array(
'filter_caption' => array(
'status' => 1,
),
),
));
$filtered_html_format->save();
// Create a node type.
$this->drupalCreateContentType(array(
'type' => 'article',
'name' => 'Article',
));
// Create one node of the above node type using the above text format.
$this->drupalCreateNode(array(
'type' => 'article',
'body' => array(
0 => array(
'value' => '<p>Do you also love Drupal?</p><img src="druplicon.png" data-caption="Druplicon" />',
'format' => 'filtered_html',
)
)
));
}
/**
* Test loading of untransformed text when a user doesn't have access to it.
*/
public function testUsersWithoutPermission() {
// Create 3 users, each with insufficient permissions, i.e. without either
// or both of the following permissions:
// - the 'access in-place editing' permission
// - the 'edit any article content' permission (necessary to edit node 1)
$users = array(
$this->drupalCreateUser(static::$basicPermissions),
$this->drupalCreateUser(array_merge(static::$basicPermissions, array('edit any article content'))),
$this->drupalCreateUser(array_merge(static::$basicPermissions, array('access in-place editing')))
);
// Now test with each of the 3 users with insufficient permissions.
foreach ($users as $user) {
$this->drupalLogin($user);
$this->drupalGet('node/1');
// Ensure the text is transformed.
$this->assertRaw('<p>Do you also love Drupal?</p><figure class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
// Retrieving the untransformed text should result in an empty 403 response.
$response = $this->drupalPost('editor/' . 'node/1/body/en/full', '', array(), array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax')));
$this->assertResponse(403);
$this->assertIdentical('{}', $response);
}
}
/**
* Test loading of untransformed text when a user does have access to it.
*/
public function testUserWithPermission() {
$user = $this->drupalCreateUser(array_merge(static::$basicPermissions, array('edit any article content', 'access in-place editing')));
$this->drupalLogin($user);
$this->drupalGet('node/1');
// Ensure the text is transformed.
$this->assertRaw('<p>Do you also love Drupal?</p><figure class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
$response = $this->drupalPost('editor/' . 'node/1/body/en/full', 'application/vnd.drupal-ajax', array());
$this->assertResponse(200);
$ajax_commands = Json::decode($response);
$this->assertIdentical(1, count($ajax_commands), 'The untransformed text POST request results in one AJAX command.');
$this->assertIdentical('editorGetUntransformedText', $ajax_commands[0]['command'], 'The first AJAX command is an editorGetUntransformedText command.');
$this->assertIdentical('<p>Do you also love Drupal?</p><img src="druplicon.png" data-caption="Druplicon" />', $ajax_commands[0]['data'], 'The editorGetUntransformedText command contains the expected data.');
}
}

View file

@ -0,0 +1,234 @@
<?php
/**
* @file
* Contains \Drupal\editor\Tests\QuickEditIntegrationTest.
*/
namespace Drupal\editor\Tests;
use Drupal\Component\Serialization\Json;
use Drupal\Core\EventSubscriber\AjaxResponseSubscriber;
use Drupal\Core\Language\LanguageInterface;
use Drupal\quickedit\EditorSelector;
use Drupal\quickedit\MetadataGenerator;
use Drupal\quickedit\Plugin\InPlaceEditorManager;
use Drupal\quickedit\Tests\QuickEditTestBase;
use Drupal\quickedit_test\MockEditEntityFieldAccessCheck;
use Drupal\editor\EditorController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* Tests Edit module integration (Editor module's inline editing support).
*
* @group editor
*/
class QuickEditIntegrationTest extends QuickEditTestBase {
/**
* {@inheritdoc}
*/
public static $modules = array('editor', 'editor_test');
/**
* The manager for editor plug-ins.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $editorManager;
/**
* The metadata generator object to be tested.
*
* @var \Drupal\quickedit\MetadataGeneratorInterface.php
*/
protected $metadataGenerator;
/**
* The editor selector object to be used by the metadata generator object.
*
* @var \Drupal\quickedit\EditorSelectorInterface
*/
protected $editorSelector;
/**
* The access checker object to be used by the metadata generator object.
*
* @var \Drupal\quickedit\Access\EditEntityFieldAccessCheckInterface
*/
protected $accessChecker;
/**
* The name of the field ued for tests.
*
* @var string
*/
protected $fieldName;
protected function setUp() {
parent::setUp();
// Install the Filter module.
$this->installSchema('system', 'url_alias');
// Create a field.
$this->fieldName = 'field_textarea';
$this->createFieldWithStorage(
$this->fieldName, 'text', 1, 'Long text field',
// Instance settings.
array(),
// Widget type & settings.
'text_textarea',
array('size' => 42),
// 'default' formatter type & settings.
'text_default',
array()
);
// Create text format.
$full_html_format = entity_create('filter_format', array(
'format' => 'full_html',
'name' => 'Full HTML',
'weight' => 1,
'filters' => array(),
));
$full_html_format->save();
// Associate text editor with text format.
$editor = entity_create('editor', array(
'format' => $full_html_format->id(),
'editor' => 'unicorn',
));
$editor->save();
// Also create a text format without an associated text editor.
entity_create('filter_format', array(
'format' => 'no_editor',
'name' => 'No Text Editor',
'weight' => 2,
'filters' => array(),
))->save();
}
/**
* Returns the in-place editor that Edit selects.
*/
protected function getSelectedEditor($entity_id, $field_name, $view_mode = 'default') {
$entity = entity_load('entity_test', $entity_id, TRUE);
$items = $entity->getTranslation(LanguageInterface::LANGCODE_NOT_SPECIFIED)->get($field_name);
$options = entity_get_display('entity_test', 'entity_test', $view_mode)->getComponent($field_name);
return $this->editorSelector->getEditor($options['type'], $items);
}
/**
* Tests editor selection when the Editor module is present.
*
* Tests a textual field, with text filtering, with cardinality 1 and >1,
* always with a ProcessedTextEditor plug-in present, but with varying text
* format compatibility.
*/
public function testEditorSelection() {
$this->editorManager = $this->container->get('plugin.manager.quickedit.editor');
$this->editorSelector = $this->container->get('quickedit.editor.selector');
// Create an entity with values for this text field.
$entity = entity_create('entity_test');
$entity->{$this->fieldName}->value = 'Hello, world!';
$entity->{$this->fieldName}->format = 'filtered_html';
$entity->save();
// Editor selection w/ cardinality 1, text format w/o associated text editor.
$this->assertEqual('form', $this->getSelectedEditor($entity->id(), $this->fieldName), "With cardinality 1, and the filtered_html text format, the 'form' editor is selected.");
// Editor selection w/ cardinality 1, text format w/ associated text editor.
$entity->{$this->fieldName}->format = 'full_html';
$entity->save();
$this->assertEqual('editor', $this->getSelectedEditor($entity->id(), $this->fieldName), "With cardinality 1, and the full_html text format, the 'editor' editor is selected.");
// Editor selection with text processing, cardinality >1
$this->fields->field_textarea_field_storage->setCardinality(2);
$this->fields->field_textarea_field_storage->save();
$this->assertEqual('form', $this->getSelectedEditor($entity->id(), $this->fieldName), "With cardinality >1, and both items using the full_html text format, the 'form' editor is selected.");
}
/**
* Tests (custom) metadata when the formatted text editor is used.
*/
public function testMetadata() {
$this->editorManager = $this->container->get('plugin.manager.quickedit.editor');
$this->accessChecker = new MockEditEntityFieldAccessCheck();
$this->editorSelector = $this->container->get('quickedit.editor.selector');
$this->metadataGenerator = new MetadataGenerator($this->accessChecker, $this->editorSelector, $this->editorManager);
// Create an entity with values for the field.
$entity = entity_create('entity_test');
$entity->{$this->fieldName}->value = 'Test';
$entity->{$this->fieldName}->format = 'full_html';
$entity->save();
$entity = entity_load('entity_test', $entity->id());
// Verify metadata.
$items = $entity->getTranslation(LanguageInterface::LANGCODE_NOT_SPECIFIED)->get($this->fieldName);
$metadata = $this->metadataGenerator->generateFieldMetadata($items, 'default');
$expected = array(
'access' => TRUE,
'label' => 'Long text field',
'editor' => 'editor',
'aria' => 'Entity entity_test 1, field Long text field',
'custom' => array(
'format' => 'full_html',
'formatHasTransformations' => FALSE,
),
);
$this->assertEqual($expected, $metadata, 'The correct metadata (including custom metadata) is generated.');
}
/**
* Tests in-place editor attachments when the Editor module is present.
*/
public function testAttachments() {
$this->editorSelector = $this->container->get('quickedit.editor.selector');
$editors = array('editor');
$attachments = $this->editorSelector->getEditorAttachments($editors);
$this->assertIdentical($attachments, array('library' => array('editor/quickedit.inPlaceEditor.formattedText')), "Expected attachments for Editor module's in-place editor found.");
}
/**
* Tests GetUntransformedTextCommand AJAX command.
*/
public function testGetUntransformedTextCommand() {
// Create an entity with values for the field.
$entity = entity_create('entity_test');
$entity->{$this->fieldName}->value = 'Test';
$entity->{$this->fieldName}->format = 'full_html';
$entity->save();
$entity = entity_load('entity_test', $entity->id());
// Verify AJAX response.
$controller = new EditorController();
$request = new Request();
$response = $controller->getUntransformedText($entity, $this->fieldName, LanguageInterface::LANGCODE_NOT_SPECIFIED, 'default');
$expected = array(
array(
'command' => 'editorGetUntransformedText',
'data' => 'Test',
)
);
$ajax_response_attachments_processor = \Drupal::service('ajax_response.attachments_processor');
$subscriber = new AjaxResponseSubscriber($ajax_response_attachments_processor);
$event = new FilterResponseEvent(
\Drupal::service('http_kernel'),
$request,
HttpKernelInterface::MASTER_REQUEST,
$response
);
$subscriber->onResponse($event);
$this->assertEqual(Json::encode($expected), $response->getContent(), 'The GetUntransformedTextCommand AJAX command works correctly.');
}
}