Move into nested docroot
This commit is contained in:
parent
83a0d3a149
commit
c8b70abde9
13405 changed files with 0 additions and 0 deletions
42
web/core/modules/editor/src/Ajax/EditorDialogSave.php
Normal file
42
web/core/modules/editor/src/Ajax/EditorDialogSave.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Ajax;
|
||||
|
||||
use Drupal\Core\Ajax\CommandInterface;
|
||||
|
||||
/**
|
||||
* Provides an AJAX command for saving the contents of an editor dialog.
|
||||
*
|
||||
* This command is implemented in editor.dialog.js in
|
||||
* Drupal.AjaxCommands.prototype.editorDialogSave.
|
||||
*/
|
||||
class EditorDialogSave implements CommandInterface {
|
||||
|
||||
/**
|
||||
* An array of values that will be passed back to the editor by the dialog.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $values;
|
||||
|
||||
/**
|
||||
* Constructs a EditorDialogSave object.
|
||||
*
|
||||
* @param string $values
|
||||
* The values that should be passed to the form constructor in Drupal.
|
||||
*/
|
||||
public function __construct($values) {
|
||||
$this->values = $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function render() {
|
||||
return array(
|
||||
'command' => 'editorDialogSave',
|
||||
'values' => $this->values,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Ajax;
|
||||
|
||||
use Drupal\Core\Ajax\BaseCommand;
|
||||
|
||||
/**
|
||||
* AJAX command to rerender a formatted text field without any transformation
|
||||
* filters.
|
||||
*/
|
||||
class GetUntransformedTextCommand extends BaseCommand {
|
||||
|
||||
/**
|
||||
* Constructs a GetUntransformedTextCommand object.
|
||||
*
|
||||
* @param string $data
|
||||
* The data to pass on to the client side.
|
||||
*/
|
||||
public function __construct($data) {
|
||||
parent::__construct('editorGetUntransformedText', $data);
|
||||
}
|
||||
|
||||
}
|
||||
99
web/core/modules/editor/src/Annotation/Editor.php
Normal file
99
web/core/modules/editor/src/Annotation/Editor.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Annotation;
|
||||
|
||||
use Drupal\Component\Annotation\Plugin;
|
||||
|
||||
/**
|
||||
* Defines an Editor annotation object.
|
||||
*
|
||||
* Plugin Namespace: Plugin\Editor
|
||||
*
|
||||
* Text editor plugin implementations need to define a plugin definition array
|
||||
* through annotation. These definition arrays may be altered through
|
||||
* hook_editor_info_alter(). The definition includes the following keys:
|
||||
*
|
||||
* - id: The unique, system-wide identifier of the text editor. Typically named
|
||||
* the same as the editor library.
|
||||
* - label: The human-readable name of the text editor, translated.
|
||||
* - supports_content_filtering: Whether the editor supports "allowed content
|
||||
* only" filtering.
|
||||
* - supports_inline_editing: Whether the editor supports the inline editing
|
||||
* provided by the Edit module.
|
||||
* - is_xss_safe: Whether this text editor is not vulnerable to XSS attacks.
|
||||
* - supported_element_types: On which form element #types this text editor is
|
||||
* capable of working.
|
||||
*
|
||||
* A complete sample plugin definition should be defined as in this example:
|
||||
*
|
||||
* @code
|
||||
* @Editor(
|
||||
* id = "myeditor",
|
||||
* label = @Translation("My Editor"),
|
||||
* supports_content_filtering = FALSE,
|
||||
* supports_inline_editing = FALSE,
|
||||
* is_xss_safe = FALSE,
|
||||
* supported_element_types = {
|
||||
* "textarea",
|
||||
* "textfield",
|
||||
* }
|
||||
* )
|
||||
* @endcode
|
||||
*
|
||||
* For a working example, see \Drupal\ckeditor\Plugin\Editor\CKEditor
|
||||
*
|
||||
* @see \Drupal\editor\Plugin\EditorPluginInterface
|
||||
* @see \Drupal\editor\Plugin\EditorBase
|
||||
* @see \Drupal\editor\Plugin\EditorManager
|
||||
* @see hook_editor_info_alter()
|
||||
* @see plugin_api
|
||||
*
|
||||
* @Annotation
|
||||
*/
|
||||
class Editor extends Plugin {
|
||||
|
||||
/**
|
||||
* The plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* The human-readable name of the editor plugin.
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\Translation
|
||||
*/
|
||||
public $label;
|
||||
|
||||
/**
|
||||
* Whether the editor supports "allowed content only" filtering.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $supports_content_filtering;
|
||||
|
||||
/**
|
||||
* Whether the editor supports the inline editing provided by the Edit module.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $supports_inline_editing;
|
||||
|
||||
/**
|
||||
* Whether this text editor is not vulnerable to XSS attacks.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $is_xss_safe;
|
||||
|
||||
/**
|
||||
* A list of element types this text editor supports.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $supported_element_types;
|
||||
|
||||
}
|
||||
24
web/core/modules/editor/src/EditorAccessControlHandler.php
Normal file
24
web/core/modules/editor/src/EditorAccessControlHandler.php
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor;
|
||||
|
||||
use Drupal\Core\Entity\EntityAccessControlHandler;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Defines the access control handler for the text editor entity type.
|
||||
*
|
||||
* @see \Drupal\editor\Entity\Editor
|
||||
*/
|
||||
class EditorAccessControlHandler extends EntityAccessControlHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkAccess(EntityInterface $editor, $operation, AccountInterface $account) {
|
||||
/** @var \Drupal\editor\EditorInterface $editor */
|
||||
return $editor->getFilterFormat()->access($operation, $account, TRUE);
|
||||
}
|
||||
|
||||
}
|
||||
81
web/core/modules/editor/src/EditorController.php
Normal file
81
web/core/modules/editor/src/EditorController.php
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor;
|
||||
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\editor\Ajax\GetUntransformedTextCommand;
|
||||
use Drupal\filter\Plugin\FilterInterface;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Returns responses for Editor module routes.
|
||||
*/
|
||||
class EditorController extends ControllerBase {
|
||||
|
||||
/**
|
||||
* Returns an Ajax response to render a text field without transformation filters.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityInterface $entity
|
||||
* The entity of which a formatted text field is being rerendered.
|
||||
* @param string $field_name
|
||||
* The name of the (formatted text) field that is being rerendered
|
||||
* @param string $langcode
|
||||
* The name of the language for which the formatted text field is being
|
||||
* rerendered.
|
||||
* @param string $view_mode_id
|
||||
* The view mode the formatted text field should be rerendered in.
|
||||
*
|
||||
* @return \Drupal\Core\Ajax\AjaxResponse
|
||||
* The Ajax response.
|
||||
*/
|
||||
public function getUntransformedText(EntityInterface $entity, $field_name, $langcode, $view_mode_id) {
|
||||
$response = new AjaxResponse();
|
||||
|
||||
// Direct text editing is only supported for single-valued fields.
|
||||
$field = $entity->getTranslation($langcode)->$field_name;
|
||||
$editable_text = check_markup($field->value, $field->format, $langcode, array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE));
|
||||
$response->addCommand(new GetUntransformedTextCommand($editable_text));
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the necessary XSS filtering for using a certain text format's editor.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The current request object.
|
||||
* @param \Drupal\filter\FilterFormatInterface $filter_format
|
||||
* The text format whose text editor (if any) will be used.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse
|
||||
* A JSON response containing the XSS-filtered value.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
* Thrown if no value to filter is specified.
|
||||
*
|
||||
* @see editor_filter_xss()
|
||||
*/
|
||||
public function filterXss(Request $request, FilterFormatInterface $filter_format) {
|
||||
$value = $request->request->get('value');
|
||||
if (!isset($value)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
// The original_format parameter will only exist when switching text format.
|
||||
$original_format_id = $request->request->get('original_format_id');
|
||||
$original_format = NULL;
|
||||
if (isset($original_format_id)) {
|
||||
$original_format = $this->entityManager()
|
||||
->getStorage('filter_format')
|
||||
->load($original_format_id);
|
||||
}
|
||||
|
||||
return new JsonResponse(editor_filter_xss($value, $filter_format, $original_format));
|
||||
}
|
||||
|
||||
}
|
||||
85
web/core/modules/editor/src/EditorInterface.php
Normal file
85
web/core/modules/editor/src/EditorInterface.php
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface defining a text editor entity.
|
||||
*/
|
||||
interface EditorInterface extends ConfigEntityInterface {
|
||||
|
||||
/**
|
||||
* Returns whether this text editor has an associated filter format.
|
||||
*
|
||||
* A text editor may be created at the same time as the filter format it's
|
||||
* going to be associated with; in that case, no filter format object is
|
||||
* available yet.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasAssociatedFilterFormat();
|
||||
|
||||
/**
|
||||
* Returns the filter format this text editor is associated with.
|
||||
*
|
||||
* This could be NULL if the associated filter format is still being created.
|
||||
* @see hasAssociatedFilterFormat()
|
||||
*
|
||||
* @return \Drupal\filter\FilterFormatInterface|null
|
||||
*/
|
||||
public function getFilterFormat();
|
||||
|
||||
/**
|
||||
* Returns the associated text editor plugin ID.
|
||||
*
|
||||
* @return string
|
||||
* The text editor plugin ID.
|
||||
*/
|
||||
public function getEditor();
|
||||
|
||||
/**
|
||||
* Set the text editor plugin ID.
|
||||
*
|
||||
* @param string $editor
|
||||
* The text editor plugin ID to set.
|
||||
*/
|
||||
public function setEditor($editor);
|
||||
|
||||
/**
|
||||
* Returns the text editor plugin-specific settings.
|
||||
*
|
||||
* @return array
|
||||
* A structured array containing all text editor settings.
|
||||
*/
|
||||
public function getSettings();
|
||||
|
||||
/**
|
||||
* Sets the text editor plugin-specific settings.
|
||||
*
|
||||
* @param array $settings
|
||||
* The structured array containing all text editor settings.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setSettings(array $settings);
|
||||
|
||||
/**
|
||||
* Returns the image upload settings.
|
||||
*
|
||||
* @return array
|
||||
* A structured array containing image upload settings.
|
||||
*/
|
||||
public function getImageUploadSettings();
|
||||
|
||||
/**
|
||||
* Sets the image upload settings.
|
||||
*
|
||||
* @param array $image_upload
|
||||
* The structured array containing image upload settings.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setImageUploadSettings(array $image_upload);
|
||||
|
||||
}
|
||||
173
web/core/modules/editor/src/EditorXssFilter/Standard.php
Normal file
173
web/core/modules/editor/src/EditorXssFilter/Standard.php
Normal file
|
|
@ -0,0 +1,173 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\EditorXssFilter;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\Xss;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Drupal\editor\EditorXssFilterInterface;
|
||||
|
||||
/**
|
||||
* Defines the standard text editor XSS filter.
|
||||
*/
|
||||
class Standard extends Xss implements EditorXssFilterInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function filterXss($html, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL) {
|
||||
// Apply XSS filtering, but blacklist the <script>, <style>, <link>, <embed>
|
||||
// and <object> tags.
|
||||
// The <script> and <style> tags are blacklisted because their contents
|
||||
// can be malicious (and therefor they are inherently unsafe), whereas for
|
||||
// all other tags, only their attributes can make them malicious. Since
|
||||
// \Drupal\Component\Utility\Xss::filter() protects against malicious
|
||||
// attributes, we take no blacklisting action.
|
||||
// The exceptions to the above rule are <link>, <embed> and <object>:
|
||||
// - <link> because the href attribute allows the attacker to import CSS
|
||||
// using the HTTP(S) protocols which Xss::filter() considers safe by
|
||||
// default. The imported remote CSS is applied to the main document, thus
|
||||
// allowing for the same XSS attacks as a regular <style> tag.
|
||||
// - <embed> and <object> because these tags allow non-HTML applications or
|
||||
// content to be embedded using the src or data attributes, respectively.
|
||||
// This is safe in the case of HTML documents, but not in the case of
|
||||
// Flash objects for example, that may access/modify the main document
|
||||
// directly.
|
||||
// <iframe> is considered safe because it only allows HTML content to be
|
||||
// embedded, hence ensuring the same origin policy always applies.
|
||||
$dangerous_tags = array('script', 'style', 'link', 'embed', 'object');
|
||||
|
||||
// Simply blacklisting these five dangerous tags would bring safety, but
|
||||
// also user frustration: what if a text format is configured to allow
|
||||
// <embed>, for example? Then we would strip that tag, even though it is
|
||||
// allowed, thereby causing data loss!
|
||||
// Therefor, we want to be smarter still. We want to take into account which
|
||||
// HTML tags are allowed and forbidden by the text format we're filtering
|
||||
// for, and if we're switching from another text format, we want to take
|
||||
// that format's allowed and forbidden tags into account as well.
|
||||
// In other words: we only expect markup allowed in both the original and
|
||||
// the new format to continue to exist.
|
||||
$format_restrictions = $format->getHtmlRestrictions();
|
||||
if ($original_format !== NULL) {
|
||||
$original_format_restrictions = $original_format->getHtmlRestrictions();
|
||||
}
|
||||
|
||||
// Any tags that are explicitly blacklisted by the text format must be
|
||||
// appended to the list of default dangerous tags: if they're explicitly
|
||||
// forbidden, then we must respect that configuration.
|
||||
// When switching from another text format, we must use the union of
|
||||
// forbidden tags: if either text format is more restrictive, then the
|
||||
// safety expectations of *both* text formats apply.
|
||||
$forbidden_tags = self::getForbiddenTags($format_restrictions);
|
||||
if ($original_format !== NULL) {
|
||||
$forbidden_tags = array_merge($forbidden_tags, self::getForbiddenTags($original_format_restrictions));
|
||||
}
|
||||
|
||||
// Any tags that are explicitly whitelisted by the text format must be
|
||||
// removed from the list of default dangerous tags: if they're explicitly
|
||||
// allowed, then we must respect that configuration.
|
||||
// When switching from another format, we must use the intersection of
|
||||
// allowed tags: if either format is more restrictive, then the safety
|
||||
// expectations of *both* formats apply.
|
||||
$allowed_tags = self::getAllowedTags($format_restrictions);
|
||||
if ($original_format !== NULL) {
|
||||
$allowed_tags = array_intersect($allowed_tags, self::getAllowedTags($original_format_restrictions));
|
||||
}
|
||||
|
||||
// Don't blacklist dangerous tags that are explicitly allowed in both text
|
||||
// formats.
|
||||
$blacklisted_tags = array_diff($dangerous_tags, $allowed_tags);
|
||||
|
||||
// Also blacklist tags that are explicitly forbidden in either text format.
|
||||
$blacklisted_tags = array_merge($blacklisted_tags, $forbidden_tags);
|
||||
|
||||
$output = static::filter($html, $blacklisted_tags);
|
||||
|
||||
// Since data-attributes can contain encoded HTML markup that could be
|
||||
// decoded and interpreted by editors, we need to apply XSS filtering to
|
||||
// their contents.
|
||||
return static::filterXssDataAttributes($output);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a very permissive XSS/HTML filter to data-attributes.
|
||||
*
|
||||
* @param string $html
|
||||
* The string to apply the data-attributes filtering to.
|
||||
*
|
||||
* @return string
|
||||
* The filtered string.
|
||||
*/
|
||||
protected static function filterXssDataAttributes($html) {
|
||||
if (stristr($html, 'data-') !== FALSE) {
|
||||
$dom = Html::load($html);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
foreach ($xpath->query('//@*[starts-with(name(.), "data-")]') as $node) {
|
||||
// The data-attributes contain an HTML-encoded value, so we need to
|
||||
// decode the value, apply XSS filtering and then re-save as encoded
|
||||
// value. There is no need to explicitly decode $node->value, since the
|
||||
// DOMAttr::value getter returns the decoded value.
|
||||
$value = Xss::filterAdmin($node->value);
|
||||
$node->value = Html::escape($value);
|
||||
}
|
||||
$html = Html::serialize($dom);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all allowed tags from a restrictions data structure.
|
||||
*
|
||||
* @param array|false $restrictions
|
||||
* Restrictions as returned by FilterInterface::getHTMLRestrictions().
|
||||
*
|
||||
* @return array
|
||||
* An array of allowed HTML tags.
|
||||
*
|
||||
* @see \Drupal\filter\Plugin\Filter\FilterInterface::getHTMLRestrictions()
|
||||
*/
|
||||
protected static function getAllowedTags($restrictions) {
|
||||
if ($restrictions === FALSE || !isset($restrictions['allowed'])) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$allowed_tags = array_keys($restrictions['allowed']);
|
||||
// Exclude the wildcard tag, which is used to set attribute restrictions on
|
||||
// all tags simultaneously.
|
||||
$allowed_tags = array_diff($allowed_tags, array('*'));
|
||||
|
||||
return $allowed_tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all forbidden tags from a restrictions data structure.
|
||||
*
|
||||
* @param array|false $restrictions
|
||||
* Restrictions as returned by FilterInterface::getHTMLRestrictions().
|
||||
*
|
||||
* @return array
|
||||
* An array of forbidden HTML tags.
|
||||
*
|
||||
* @see \Drupal\filter\Plugin\Filter\FilterInterface::getHTMLRestrictions()
|
||||
*/
|
||||
protected static function getForbiddenTags($restrictions) {
|
||||
if ($restrictions === FALSE || !isset($restrictions['forbidden_tags'])) {
|
||||
return array();
|
||||
}
|
||||
else {
|
||||
return $restrictions['forbidden_tags'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static function needsRemoval($html_tags, $elem) {
|
||||
// See static::filterXss() about how this class uses blacklisting instead
|
||||
// of the normal whitelisting.
|
||||
return !parent::needsRemoval($html_tags, $elem);
|
||||
}
|
||||
|
||||
}
|
||||
42
web/core/modules/editor/src/EditorXssFilterInterface.php
Normal file
42
web/core/modules/editor/src/EditorXssFilterInterface.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor;
|
||||
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
|
||||
/**
|
||||
* Defines an interface for text editor XSS (Cross-site scripting) filters.
|
||||
*/
|
||||
interface EditorXssFilterInterface {
|
||||
|
||||
/**
|
||||
* Filters HTML to prevent XSS attacks when a user edits it in a text editor.
|
||||
*
|
||||
* Should filter as minimally as possible, only to remove XSS attack vectors.
|
||||
*
|
||||
* Is only called when:
|
||||
* - loading a non-XSS-safe text editor for a $format that contains a filter
|
||||
* preventing XSS attacks (a FilterInterface::TYPE_HTML_RESTRICTOR filter):
|
||||
* if the output is safe, it should also be safe to edit.
|
||||
* - loading a non-XSS-safe text editor for a $format that doesn't contain a
|
||||
* filter preventing XSS attacks, but we're switching from a previous text
|
||||
* format ($original_format is not NULL) that did prevent XSS attacks: if
|
||||
* the output was previously safe, it should be safe to switch to another
|
||||
* text format and edit.
|
||||
*
|
||||
* @param string $html
|
||||
* The HTML to be filtered.
|
||||
* @param \Drupal\filter\FilterFormatInterface $format
|
||||
* The text format configuration entity. Provides context based upon which
|
||||
* one may want to adjust the filtering.
|
||||
* @param \Drupal\filter\FilterFormatInterface|null $original_format
|
||||
* (optional) The original text format configuration entity (when switching
|
||||
* text formats/editors). Also provides context based upon which one may
|
||||
* want to adjust the filtering.
|
||||
*
|
||||
* @return string
|
||||
* The filtered HTML that cannot cause any XSSes anymore.
|
||||
*/
|
||||
public static function filterXss($html, FilterFormatInterface $format, FilterFormatInterface $original_format = NULL);
|
||||
|
||||
}
|
||||
118
web/core/modules/editor/src/Element.php
Normal file
118
web/core/modules/editor/src/Element.php
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
|
||||
/**
|
||||
* Defines a service for Text Editor's render elements.
|
||||
*/
|
||||
class Element {
|
||||
|
||||
/**
|
||||
* The Text Editor plugin manager service.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected $pluginManager;
|
||||
|
||||
/**
|
||||
* Constructs a new Element object.
|
||||
*
|
||||
* @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
|
||||
* The Text Editor plugin manager service.
|
||||
*/
|
||||
public function __construct(PluginManagerInterface $plugin_manager) {
|
||||
$this->pluginManager = $plugin_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional #pre_render callback for 'text_format' elements.
|
||||
*/
|
||||
function preRenderTextFormat(array $element) {
|
||||
// Allow modules to programmatically enforce no client-side editor by
|
||||
// setting the #editor property to FALSE.
|
||||
if (isset($element['#editor']) && !$element['#editor']) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
// filter_process_format() copies properties to the expanded 'value' child
|
||||
// element, including the #pre_render property. Skip this text format
|
||||
// widget, if it contains no 'format'.
|
||||
if (!isset($element['format'])) {
|
||||
return $element;
|
||||
}
|
||||
$format_ids = array_keys($element['format']['format']['#options']);
|
||||
|
||||
// Early-return if no text editor is associated with any of the text formats.
|
||||
$editors = Editor::loadMultiple($format_ids);
|
||||
foreach ($editors as $key => $editor) {
|
||||
$definition = $this->pluginManager->getDefinition($editor->getEditor());
|
||||
if (!in_array($element['#base_type'], $definition['supported_element_types'])) {
|
||||
unset($editors[$key]);
|
||||
}
|
||||
}
|
||||
if (count($editors) === 0) {
|
||||
return $element;
|
||||
}
|
||||
|
||||
// Use a hidden element for a single text format.
|
||||
$field_id = $element['value']['#id'];
|
||||
if (!$element['format']['format']['#access']) {
|
||||
// Use the first (and only) available text format.
|
||||
$format_id = $format_ids[0];
|
||||
$element['format']['editor'] = array(
|
||||
'#type' => 'hidden',
|
||||
'#name' => $element['format']['format']['#name'],
|
||||
'#value' => $format_id,
|
||||
'#attributes' => array(
|
||||
'data-editor-for' => $field_id,
|
||||
),
|
||||
);
|
||||
}
|
||||
// Otherwise, attach to text format selector.
|
||||
else {
|
||||
$element['format']['format']['#attributes']['class'][] = 'editor';
|
||||
$element['format']['format']['#attributes']['data-editor-for'] = $field_id;
|
||||
}
|
||||
|
||||
// Hide the text format's filters' guidelines of those text formats that have
|
||||
// a text editor associated: they're rather useless when using a text editor.
|
||||
foreach ($editors as $format_id => $editor) {
|
||||
$element['format']['guidelines'][$format_id]['#access'] = FALSE;
|
||||
}
|
||||
|
||||
// Attach Text Editor module's (this module) library.
|
||||
$element['#attached']['library'][] = 'editor/drupal.editor';
|
||||
|
||||
// Attach attachments for all available editors.
|
||||
$element['#attached'] = BubbleableMetadata::mergeAttachments($element['#attached'], $this->pluginManager->getAttachments($format_ids));
|
||||
|
||||
// Apply XSS filters when editing content if necessary. Some types of text
|
||||
// editors cannot guarantee that the end user won't become a victim of XSS.
|
||||
if (!empty($element['value']['#value'])) {
|
||||
$original = $element['value']['#value'];
|
||||
$format = FilterFormat::load($element['format']['format']['#value']);
|
||||
|
||||
// Ensure XSS-safety for the current text format/editor.
|
||||
$filtered = editor_filter_xss($original, $format);
|
||||
if ($filtered !== FALSE) {
|
||||
$element['value']['#value'] = $filtered;
|
||||
}
|
||||
|
||||
// Only when the user has access to multiple text formats, we must add data-
|
||||
// attributes for the original value and change tracking, because they are
|
||||
// only necessary when the end user can switch between text formats/editors.
|
||||
if ($element['format']['format']['#access']) {
|
||||
$element['value']['#attributes']['data-editor-value-is-changed'] = 'false';
|
||||
$element['value']['#attributes']['data-editor-value-original'] = $original;
|
||||
}
|
||||
}
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
}
|
||||
186
web/core/modules/editor/src/Entity/Editor.php
Normal file
186
web/core/modules/editor/src/Entity/Editor.php
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Entity;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityBase;
|
||||
use Drupal\editor\EditorInterface;
|
||||
|
||||
/**
|
||||
* Defines the configured text editor entity.
|
||||
*
|
||||
* @ConfigEntityType(
|
||||
* id = "editor",
|
||||
* label = @Translation("Text Editor"),
|
||||
* handlers = {
|
||||
* "access" = "Drupal\editor\EditorAccessControlHandler",
|
||||
* },
|
||||
* entity_keys = {
|
||||
* "id" = "format"
|
||||
* },
|
||||
* config_export = {
|
||||
* "format",
|
||||
* "editor",
|
||||
* "settings",
|
||||
* "image_upload",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class Editor extends ConfigEntityBase implements EditorInterface {
|
||||
|
||||
/**
|
||||
* The machine name of the text format with which this configured text editor
|
||||
* is associated.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @see getFilterFormat()
|
||||
*/
|
||||
protected $format;
|
||||
|
||||
/**
|
||||
* The name (plugin ID) of the text editor.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $editor;
|
||||
|
||||
/**
|
||||
* The structured array of text editor plugin-specific settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $settings = array();
|
||||
|
||||
/**
|
||||
* The structured array of image upload settings.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $image_upload = array();
|
||||
|
||||
/**
|
||||
* The filter format this text editor is associated with.
|
||||
*
|
||||
* @var \Drupal\filter\FilterFormatInterface
|
||||
*/
|
||||
protected $filterFormat;
|
||||
|
||||
/**
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected $editorPluginManager;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function id() {
|
||||
return $this->format;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(array $values, $entity_type) {
|
||||
parent::__construct($values, $entity_type);
|
||||
|
||||
$plugin = $this->editorPluginManager()->createInstance($this->editor);
|
||||
$this->settings += $plugin->getDefaultSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function label() {
|
||||
return $this->getFilterFormat()->label();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function calculateDependencies() {
|
||||
parent::calculateDependencies();
|
||||
// Create a dependency on the associated FilterFormat.
|
||||
$this->addDependency('config', $this->getFilterFormat()->getConfigDependencyName());
|
||||
// @todo use EntityWithPluginCollectionInterface so configuration between
|
||||
// config entity and dependency on provider is managed automatically.
|
||||
$definition = $this->editorPluginManager()->createInstance($this->editor)->getPluginDefinition();
|
||||
$this->addDependency('module', $definition['provider']);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function hasAssociatedFilterFormat() {
|
||||
return $this->format !== NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilterFormat() {
|
||||
if (!$this->filterFormat) {
|
||||
$this->filterFormat = \Drupal::entityManager()->getStorage('filter_format')->load($this->format);
|
||||
}
|
||||
return $this->filterFormat;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the editor plugin manager.
|
||||
*
|
||||
* @return \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected function editorPluginManager() {
|
||||
if (!$this->editorPluginManager) {
|
||||
$this->editorPluginManager = \Drupal::service('plugin.manager.editor');
|
||||
}
|
||||
|
||||
return $this->editorPluginManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getEditor() {
|
||||
return $this->editor;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setEditor($editor) {
|
||||
$this->editor = $editor;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSettings() {
|
||||
return $this->settings;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setSettings(array $settings) {
|
||||
$this->settings = $settings;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getImageUploadSettings() {
|
||||
return $this->image_upload;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setImageUploadSettings(array $image_upload_settings) {
|
||||
$this->image_upload = $image_upload_settings;
|
||||
return $this;
|
||||
}
|
||||
|
||||
}
|
||||
239
web/core/modules/editor/src/Form/EditorImageDialog.php
Normal file
239
web/core/modules/editor/src/Form/EditorImageDialog.php
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Form;
|
||||
|
||||
use Drupal\Component\Utility\Bytes;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\HtmlCommand;
|
||||
use Drupal\editor\Ajax\EditorDialogSave;
|
||||
use Drupal\Core\Ajax\CloseModalDialogCommand;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
|
||||
/**
|
||||
* Provides an image dialog for text editors.
|
||||
*/
|
||||
class EditorImageDialog extends FormBase {
|
||||
|
||||
/**
|
||||
* The file storage service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $fileStorage;
|
||||
|
||||
/**
|
||||
* Constructs a form object for image dialog.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $file_storage
|
||||
* The file storage service.
|
||||
*/
|
||||
public function __construct(EntityStorageInterface $file_storage) {
|
||||
$this->fileStorage = $file_storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity.manager')->getStorage('file')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'editor_image_dialog';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* The text editor to which this dialog corresponds.
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, Editor $editor = NULL) {
|
||||
// This form is special, in that the default values do not come from the
|
||||
// server side, but from the client side, from a text editor. We must cache
|
||||
// this data in form state, because when the form is rebuilt, we will be
|
||||
// receiving values from the form, instead of the values from the text
|
||||
// editor. If we don't cache it, this data will be lost.
|
||||
if (isset($form_state->getUserInput()['editor_object'])) {
|
||||
// By convention, the data that the text editor sends to any dialog is in
|
||||
// the 'editor_object' key. And the image dialog for text editors expects
|
||||
// that data to be the attributes for an <img> element.
|
||||
$image_element = $form_state->getUserInput()['editor_object'];
|
||||
$form_state->set('image_element', $image_element);
|
||||
$form_state->setCached(TRUE);
|
||||
}
|
||||
else {
|
||||
// Retrieve the image element's attributes from form state.
|
||||
$image_element = $form_state->get('image_element') ?: [];
|
||||
}
|
||||
|
||||
$form['#tree'] = TRUE;
|
||||
$form['#attached']['library'][] = 'editor/drupal.editor.dialog';
|
||||
$form['#prefix'] = '<div id="editor-image-dialog-form">';
|
||||
$form['#suffix'] = '</div>';
|
||||
|
||||
// Construct strings to use in the upload validators.
|
||||
$image_upload = $editor->getImageUploadSettings();
|
||||
if (!empty($image_upload['max_dimensions']['width']) || !empty($image_upload['max_dimensions']['height'])) {
|
||||
$max_dimensions = $image_upload['max_dimensions']['width'] . 'x' . $image_upload['max_dimensions']['height'];
|
||||
}
|
||||
else {
|
||||
$max_dimensions = 0;
|
||||
}
|
||||
$max_filesize = min(Bytes::toInt($image_upload['max_size']), file_upload_max_size());
|
||||
|
||||
$existing_file = isset($image_element['data-entity-uuid']) ? \Drupal::entityManager()->loadEntityByUuid('file', $image_element['data-entity-uuid']) : NULL;
|
||||
$fid = $existing_file ? $existing_file->id() : NULL;
|
||||
|
||||
$form['fid'] = array(
|
||||
'#title' => $this->t('Image'),
|
||||
'#type' => 'managed_file',
|
||||
'#upload_location' => $image_upload['scheme'] . '://' . $image_upload['directory'],
|
||||
'#default_value' => $fid ? array($fid) : NULL,
|
||||
'#upload_validators' => array(
|
||||
'file_validate_extensions' => array('gif png jpg jpeg'),
|
||||
'file_validate_size' => array($max_filesize),
|
||||
'file_validate_image_resolution' => array($max_dimensions),
|
||||
),
|
||||
'#required' => TRUE,
|
||||
);
|
||||
|
||||
$form['attributes']['src'] = array(
|
||||
'#title' => $this->t('URL'),
|
||||
'#type' => 'textfield',
|
||||
'#default_value' => isset($image_element['src']) ? $image_element['src'] : '',
|
||||
'#maxlength' => 2048,
|
||||
'#required' => TRUE,
|
||||
);
|
||||
|
||||
// If the editor has image uploads enabled, show a managed_file form item,
|
||||
// otherwise show a (file URL) text form item.
|
||||
if ($image_upload['status']) {
|
||||
$form['attributes']['src']['#access'] = FALSE;
|
||||
$form['attributes']['src']['#required'] = FALSE;
|
||||
}
|
||||
else {
|
||||
$form['fid']['#access'] = FALSE;
|
||||
$form['fid']['#required'] = FALSE;
|
||||
}
|
||||
|
||||
// The alt attribute is *required*, but we allow users to opt-in to empty
|
||||
// alt attributes for the very rare edge cases where that is valid by
|
||||
// specifying two double quotes as the alternative text in the dialog.
|
||||
// However, that *is* stored as an empty alt attribute, so if we're editing
|
||||
// an existing image (which means the src attribute is set) and its alt
|
||||
// attribute is empty, then we show that as two double quotes in the dialog.
|
||||
// @see https://www.drupal.org/node/2307647
|
||||
$alt = isset($image_element['alt']) ? $image_element['alt'] : '';
|
||||
if ($alt === '' && !empty($image_element['src'])) {
|
||||
$alt = '""';
|
||||
}
|
||||
$form['attributes']['alt'] = array(
|
||||
'#title' => $this->t('Alternative text'),
|
||||
'#placeholder' => $this->t('Short description for the visually impaired'),
|
||||
'#type' => 'textfield',
|
||||
'#required' => TRUE,
|
||||
'#required_error' => $this->t('Alternative text is required.<br />(Only in rare cases should this be left empty. To create empty alternative text, enter <code>""</code> — two double quotes without any content).'),
|
||||
'#default_value' => $alt,
|
||||
'#maxlength' => 2048,
|
||||
);
|
||||
|
||||
// When Drupal core's filter_align is being used, the text editor may
|
||||
// offer the ability to change the alignment.
|
||||
if (isset($image_element['data-align']) && $editor->getFilterFormat()->filters('filter_align')->status) {
|
||||
$form['align'] = array(
|
||||
'#title' => $this->t('Align'),
|
||||
'#type' => 'radios',
|
||||
'#options' => array(
|
||||
'none' => $this->t('None'),
|
||||
'left' => $this->t('Left'),
|
||||
'center' => $this->t('Center'),
|
||||
'right' => $this->t('Right'),
|
||||
),
|
||||
'#default_value' => $image_element['data-align'] === '' ? 'none' : $image_element['data-align'],
|
||||
'#wrapper_attributes' => array('class' => array('container-inline')),
|
||||
'#attributes' => array('class' => array('container-inline')),
|
||||
'#parents' => array('attributes', 'data-align'),
|
||||
);
|
||||
}
|
||||
|
||||
// When Drupal core's filter_caption is being used, the text editor may
|
||||
// offer the ability to in-place edit the image's caption: show a toggle.
|
||||
if (isset($image_element['hasCaption']) && $editor->getFilterFormat()->filters('filter_caption')->status) {
|
||||
$form['caption'] = array(
|
||||
'#title' => $this->t('Caption'),
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => $image_element['hasCaption'] === 'true',
|
||||
'#parents' => array('attributes', 'hasCaption'),
|
||||
);
|
||||
}
|
||||
|
||||
$form['actions'] = array(
|
||||
'#type' => 'actions',
|
||||
);
|
||||
$form['actions']['save_modal'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Save'),
|
||||
// No regular submit-handler. This form only works via JavaScript.
|
||||
'#submit' => array(),
|
||||
'#ajax' => array(
|
||||
'callback' => '::submitForm',
|
||||
'event' => 'click',
|
||||
),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$response = new AjaxResponse();
|
||||
|
||||
// Convert any uploaded files from the FID values to data-entity-uuid
|
||||
// attributes and set data-entity-type to 'file'.
|
||||
$fid = $form_state->getValue(array('fid', 0));
|
||||
if (!empty($fid)) {
|
||||
$file = $this->fileStorage->load($fid);
|
||||
$file_url = file_create_url($file->getFileUri());
|
||||
// Transform absolute image URLs to relative image URLs: prevent problems
|
||||
// on multisite set-ups and prevent mixed content errors.
|
||||
$file_url = file_url_transform_relative($file_url);
|
||||
$form_state->setValue(array('attributes', 'src'), $file_url);
|
||||
$form_state->setValue(array('attributes', 'data-entity-uuid'), $file->uuid());
|
||||
$form_state->setValue(array('attributes', 'data-entity-type'), 'file');
|
||||
}
|
||||
|
||||
// When the alt attribute is set to two double quotes, transform it to the
|
||||
// empty string: two double quotes signify "empty alt attribute". See above.
|
||||
if (trim($form_state->getValue(array('attributes', 'alt'))) === '""') {
|
||||
$form_state->setValue(array('attributes', 'alt'), '');
|
||||
}
|
||||
|
||||
if ($form_state->getErrors()) {
|
||||
unset($form['#prefix'], $form['#suffix']);
|
||||
$form['status_messages'] = [
|
||||
'#type' => 'status_messages',
|
||||
'#weight' => -10,
|
||||
];
|
||||
$response->addCommand(new HtmlCommand('#editor-image-dialog-form', $form));
|
||||
}
|
||||
else {
|
||||
$response->addCommand(new EditorDialogSave($form_state->getValues()));
|
||||
$response->addCommand(new CloseModalDialogCommand());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
90
web/core/modules/editor/src/Form/EditorLinkDialog.php
Normal file
90
web/core/modules/editor/src/Form/EditorLinkDialog.php
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Form;
|
||||
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\HtmlCommand;
|
||||
use Drupal\editor\Ajax\EditorDialogSave;
|
||||
use Drupal\Core\Ajax\CloseModalDialogCommand;
|
||||
|
||||
/**
|
||||
* Provides a link dialog for text editors.
|
||||
*/
|
||||
class EditorLinkDialog extends FormBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'editor_link_dialog';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* The text editor to which this dialog corresponds.
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, Editor $editor = NULL) {
|
||||
// The default values are set directly from \Drupal::request()->request,
|
||||
// provided by the editor plugin opening the dialog.
|
||||
$user_input = $form_state->getUserInput();
|
||||
$input = isset($user_input['editor_object']) ? $user_input['editor_object'] : array();
|
||||
|
||||
$form['#tree'] = TRUE;
|
||||
$form['#attached']['library'][] = 'editor/drupal.editor.dialog';
|
||||
$form['#prefix'] = '<div id="editor-link-dialog-form">';
|
||||
$form['#suffix'] = '</div>';
|
||||
|
||||
// Everything under the "attributes" key is merged directly into the
|
||||
// generated link tag's attributes.
|
||||
$form['attributes']['href'] = array(
|
||||
'#title' => $this->t('URL'),
|
||||
'#type' => 'textfield',
|
||||
'#default_value' => isset($input['href']) ? $input['href'] : '',
|
||||
'#maxlength' => 2048,
|
||||
);
|
||||
|
||||
$form['actions'] = array(
|
||||
'#type' => 'actions',
|
||||
);
|
||||
$form['actions']['save_modal'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Save'),
|
||||
// No regular submit-handler. This form only works via JavaScript.
|
||||
'#submit' => array(),
|
||||
'#ajax' => array(
|
||||
'callback' => '::submitForm',
|
||||
'event' => 'click',
|
||||
),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$response = new AjaxResponse();
|
||||
|
||||
if ($form_state->getErrors()) {
|
||||
unset($form['#prefix'], $form['#suffix']);
|
||||
$form['status_messages'] = [
|
||||
'#type' => 'status_messages',
|
||||
'#weight' => -10,
|
||||
];
|
||||
$response->addCommand(new HtmlCommand('#editor-link-dialog-form', $form));
|
||||
}
|
||||
else {
|
||||
$response->addCommand(new EditorDialogSave($form_state->getValues()));
|
||||
$response->addCommand(new CloseModalDialogCommand());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
51
web/core/modules/editor/src/Plugin/EditorBase.php
Normal file
51
web/core/modules/editor/src/Plugin/EditorBase.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Plugin;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines a base class from which other modules providing editors may extend.
|
||||
*
|
||||
* This class provides default implementations of the EditorPluginInterface so
|
||||
* that classes extending this one do not need to implement every method.
|
||||
*
|
||||
* Plugins extending this class need to specify an annotation containing the
|
||||
* plugin definition so the plugin can be discovered.
|
||||
*
|
||||
* @see \Drupal\editor\Annotation\Editor
|
||||
* @see \Drupal\editor\Plugin\EditorPluginInterface
|
||||
* @see \Drupal\editor\Plugin\EditorManager
|
||||
* @see plugin_api
|
||||
*/
|
||||
abstract class EditorBase extends PluginBase implements EditorPluginInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultSettings() {
|
||||
return array();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor) {
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsFormValidate(array $form, FormStateInterface $form_state) {
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsFormSubmit(array $form, FormStateInterface $form_state) {
|
||||
}
|
||||
|
||||
}
|
||||
99
web/core/modules/editor/src/Plugin/EditorManager.php
Normal file
99
web/core/modules/editor/src/Plugin/EditorManager.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Plugin;
|
||||
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
|
||||
/**
|
||||
* Configurable text editor manager.
|
||||
*
|
||||
* @see \Drupal\editor\Annotation\Editor
|
||||
* @see \Drupal\editor\Plugin\EditorPluginInterface
|
||||
* @see \Drupal\editor\Plugin\EditorBase
|
||||
* @see plugin_api
|
||||
*/
|
||||
class EditorManager extends DefaultPluginManager {
|
||||
|
||||
/**
|
||||
* Constructs an EditorManager object.
|
||||
*
|
||||
* @param \Traversable $namespaces
|
||||
* An object that implements \Traversable which contains the root paths
|
||||
* keyed by the corresponding namespace to look for plugin implementations.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* Cache backend instance to use.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler to invoke the alter hook with.
|
||||
*/
|
||||
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
|
||||
parent::__construct('Plugin/Editor', $namespaces, $module_handler, 'Drupal\editor\Plugin\EditorPluginInterface', 'Drupal\editor\Annotation\Editor');
|
||||
$this->alterInfo('editor_info');
|
||||
$this->setCacheBackend($cache_backend, 'editor_plugins');
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates a key-value pair of available text editors.
|
||||
*
|
||||
* @return array
|
||||
* An array of translated text editor labels, keyed by ID.
|
||||
*/
|
||||
public function listOptions() {
|
||||
$options = array();
|
||||
foreach ($this->getDefinitions() as $key => $definition) {
|
||||
$options[$key] = $definition['label'];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves text editor libraries and JavaScript settings.
|
||||
*
|
||||
* @param array $format_ids
|
||||
* An array of format IDs as returned by array_keys(filter_formats()).
|
||||
*
|
||||
* @return array
|
||||
* An array of attachments, for use with #attached.
|
||||
*
|
||||
* @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments()
|
||||
*/
|
||||
public function getAttachments(array $format_ids) {
|
||||
$attachments = array('library' => array());
|
||||
|
||||
$settings = array();
|
||||
foreach ($format_ids as $format_id) {
|
||||
$editor = editor_load($format_id);
|
||||
if (!$editor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$plugin = $this->createInstance($editor->getEditor());
|
||||
$plugin_definition = $plugin->getPluginDefinition();
|
||||
|
||||
// Libraries.
|
||||
$attachments['library'] = array_merge($attachments['library'], $plugin->getLibraries($editor));
|
||||
|
||||
// Format-specific JavaScript settings.
|
||||
$settings['editor']['formats'][$format_id] = array(
|
||||
'format' => $format_id,
|
||||
'editor' => $editor->getEditor(),
|
||||
'editorSettings' => $plugin->getJSSettings($editor),
|
||||
'editorSupportsContentFiltering' => $plugin_definition['supports_content_filtering'],
|
||||
'isXssSafe' => $plugin_definition['is_xss_safe'],
|
||||
);
|
||||
}
|
||||
|
||||
// Allow other modules to alter all JavaScript settings.
|
||||
$this->moduleHandler->alter('editor_js_settings', $settings);
|
||||
|
||||
if (empty($attachments['library']) && empty($settings)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$attachments['drupalSettings'] = $settings;
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
}
|
||||
117
web/core/modules/editor/src/Plugin/EditorPluginInterface.php
Normal file
117
web/core/modules/editor/src/Plugin/EditorPluginInterface.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Plugin;
|
||||
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines an interface for configurable text editors.
|
||||
*
|
||||
* Modules implementing this interface may want to extend the EditorBase class,
|
||||
* which provides default implementations of each method where appropriate.
|
||||
*
|
||||
* @see \Drupal\editor\Annotation\Editor
|
||||
* @see \Drupal\editor\Plugin\EditorBase
|
||||
* @see \Drupal\editor\Plugin\EditorManager
|
||||
* @see plugin_api
|
||||
*/
|
||||
interface EditorPluginInterface extends PluginInspectionInterface {
|
||||
|
||||
/**
|
||||
* Returns the default settings for this configurable text editor.
|
||||
*
|
||||
* @return array
|
||||
* An array of settings as they would be stored by a configured text editor
|
||||
* entity (\Drupal\editor\Entity\Editor).
|
||||
*/
|
||||
public function getDefaultSettings();
|
||||
|
||||
/**
|
||||
* Returns a settings form to configure this text editor.
|
||||
*
|
||||
* If the editor's behavior depends on extensive options and/or external data,
|
||||
* then the implementing module can choose to provide a separate, global
|
||||
* configuration page rather than per-text-format settings. In that case, this
|
||||
* form should provide a link to the separate settings page.
|
||||
*
|
||||
* @param array $form
|
||||
* An empty form array to be populated with a configuration form, if any.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The state of the entire filter administration form.
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return array
|
||||
* A render array for the settings form.
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state, Editor $editor);
|
||||
|
||||
/**
|
||||
* Validates the settings form for an editor.
|
||||
*
|
||||
* The contents of the editor settings are located in
|
||||
* $form_state->getValue(array('editor', 'settings')). Calls to $form_state->setError()
|
||||
* should reflect this location in the settings form.
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public function settingsFormValidate(array $form, FormStateInterface $form_state);
|
||||
|
||||
/**
|
||||
* Modifies any values in the form state to prepare them for saving.
|
||||
*
|
||||
* Values in $form_state->getValue(array('editor', 'settings')) are saved by
|
||||
* Editor module in editor_form_filter_admin_format_submit().
|
||||
*
|
||||
* @param array $form
|
||||
* An associative array containing the structure of the form.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The current state of the form.
|
||||
*/
|
||||
public function settingsFormSubmit(array $form, FormStateInterface $form_state);
|
||||
|
||||
/**
|
||||
* Returns JavaScript settings to be attached.
|
||||
*
|
||||
* Most text editors use JavaScript to provide a WYSIWYG or toolbar on the
|
||||
* client-side interface. This method can be used to convert internal settings
|
||||
* of the text editor into JavaScript variables that will be accessible when
|
||||
* the text editor is loaded.
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return array
|
||||
* An array of settings that will be added to the page for use by this text
|
||||
* editor's JavaScript integration.
|
||||
*
|
||||
* @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments()
|
||||
* @see EditorManager::getAttachments()
|
||||
*/
|
||||
public function getJSSettings(Editor $editor);
|
||||
|
||||
/**
|
||||
* Returns libraries to be attached.
|
||||
*
|
||||
* Because this is a method, plugins can dynamically choose to attach a
|
||||
* different library for different configurations, instead of being forced to
|
||||
* always use the same method.
|
||||
*
|
||||
* @param \Drupal\editor\Entity\Editor $editor
|
||||
* A configured text editor object.
|
||||
*
|
||||
* @return array
|
||||
* An array of libraries that will be added to the page for use by this text
|
||||
* editor.
|
||||
*
|
||||
* @see \Drupal\Core\Render\AttachmentsResponseProcessorInterface::processAttachments()
|
||||
* @see EditorManager::getAttachments()
|
||||
*/
|
||||
public function getLibraries(Editor $editor);
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Plugin\Filter;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Entity\EntityManagerInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\filter\FilterProcessResult;
|
||||
use Drupal\filter\Plugin\FilterBase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a filter to track images uploaded via a Text Editor.
|
||||
*
|
||||
* Generates file URLs and associates the cache tags of referenced files.
|
||||
*
|
||||
* @Filter(
|
||||
* id = "editor_file_reference",
|
||||
* title = @Translation("Track images uploaded via a Text Editor"),
|
||||
* description = @Translation("Ensures that the latest versions of images uploaded via a Text Editor are displayed."),
|
||||
* type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE
|
||||
* )
|
||||
*/
|
||||
class EditorFileReference extends FilterBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* An entity manager object.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityManagerInterface
|
||||
*/
|
||||
protected $entityManager;
|
||||
|
||||
/**
|
||||
* Constructs a \Drupal\editor\Plugin\Filter\EditorFileReference object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
|
||||
* An entity manager object.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager) {
|
||||
$this->entityManager = $entity_manager;
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
static public function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('entity.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process($text, $langcode) {
|
||||
$result = new FilterProcessResult($text);
|
||||
|
||||
if (stristr($text, 'data-entity-type="file"') !== FALSE) {
|
||||
$dom = Html::load($text);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$processed_uuids = array();
|
||||
foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid]') as $node) {
|
||||
$uuid = $node->getAttribute('data-entity-uuid');
|
||||
|
||||
// If there is a 'src' attribute, set it to the file entity's current
|
||||
// URL. This ensures the URL works even after the file location changes.
|
||||
if ($node->hasAttribute('src')) {
|
||||
$file = $this->entityManager->loadEntityByUuid('file', $uuid);
|
||||
if ($file) {
|
||||
$node->setAttribute('src', file_url_transform_relative(file_create_url($file->getFileUri())));
|
||||
}
|
||||
}
|
||||
|
||||
// Only process the first occurrence of each file UUID.
|
||||
if (!isset($processed_uuids[$uuid])) {
|
||||
$processed_uuids[$uuid] = TRUE;
|
||||
|
||||
$file = $this->entityManager->loadEntityByUuid('file', $uuid);
|
||||
if ($file) {
|
||||
$result->addCacheTags($file->getCacheTags());
|
||||
}
|
||||
}
|
||||
}
|
||||
$result->setProcessedText(Html::serialize($dom));
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
96
web/core/modules/editor/src/Plugin/InPlaceEditor/Editor.php
Normal file
96
web/core/modules/editor/src/Plugin/InPlaceEditor/Editor.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Plugin\InPlaceEditor;
|
||||
|
||||
use Drupal\Component\Plugin\PluginBase;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\quickedit\Plugin\InPlaceEditorInterface;
|
||||
use Drupal\filter\Plugin\FilterInterface;
|
||||
|
||||
/**
|
||||
* Defines the formatted text in-place editor.
|
||||
*
|
||||
* @InPlaceEditor(
|
||||
* id = "editor"
|
||||
* )
|
||||
*/
|
||||
class Editor extends PluginBase implements InPlaceEditorInterface {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isCompatible(FieldItemListInterface $items) {
|
||||
$field_definition = $items->getFieldDefinition();
|
||||
|
||||
// This editor is incompatible with multivalued fields.
|
||||
if ($field_definition->getFieldStorageDefinition()->getCardinality() != 1) {
|
||||
return FALSE;
|
||||
}
|
||||
// This editor is compatible with formatted ("rich") text fields; but only
|
||||
// if there is a currently active text format, that text format has an
|
||||
// associated editor and that editor supports inline editing.
|
||||
elseif ($editor = editor_load($items[0]->format)) {
|
||||
$definition = \Drupal::service('plugin.manager.editor')->getDefinition($editor->getEditor());
|
||||
if ($definition['supports_inline_editing'] === TRUE) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
function getMetadata(FieldItemListInterface $items) {
|
||||
$format_id = $items[0]->format;
|
||||
$metadata['format'] = $format_id;
|
||||
$metadata['formatHasTransformations'] = $this->textFormatHasTransformationFilters($format_id);
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the text format has transformation filters.
|
||||
*
|
||||
* @param int $format_id
|
||||
* A text format ID.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function textFormatHasTransformationFilters($format_id) {
|
||||
$format = FilterFormat::load($format_id);
|
||||
return (bool) count(array_intersect(array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE), $format->getFiltertypes()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAttachments() {
|
||||
$user = \Drupal::currentUser();
|
||||
|
||||
$user_format_ids = array_keys(filter_formats($user));
|
||||
$manager = \Drupal::service('plugin.manager.editor');
|
||||
$definitions = $manager->getDefinitions();
|
||||
|
||||
// Filter the current user's formats to those that support inline editing.
|
||||
$formats = array();
|
||||
foreach ($user_format_ids as $format_id) {
|
||||
if ($editor = editor_load($format_id)) {
|
||||
$editor_id = $editor->getEditor();
|
||||
if (isset($definitions[$editor_id]['supports_inline_editing']) && $definitions[$editor_id]['supports_inline_editing'] === TRUE) {
|
||||
$formats[] = $format_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get the attachments for all text editors that the user might use.
|
||||
$attachments = $manager->getAttachments($formats);
|
||||
|
||||
// Also include editor.module's formatted text editor.
|
||||
$attachments['library'][] = 'editor/quickedit.inPlaceEditor.formattedText';
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
}
|
||||
236
web/core/modules/editor/src/Tests/EditorAdminTest.php
Normal file
236
web/core/modules/editor/src/Tests/EditorAdminTest.php
Normal file
|
|
@ -0,0 +1,236 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\Component\Utility\Unicode;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\node\Entity\NodeType;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* Tests administration of text editors.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorAdminTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('filter', 'editor');
|
||||
|
||||
/**
|
||||
* A user with the 'administer filters' permission.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Add text format.
|
||||
$filtered_html_format = FilterFormat::create(array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
|
||||
// Create admin user.
|
||||
$this->adminUser = $this->drupalCreateUser(array('administer filters'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests an existing format without any editors available.
|
||||
*/
|
||||
public function testNoEditorAvailable() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
|
||||
|
||||
// Ensure the form field order is correct.
|
||||
$roles_pos = strpos($this->getRawContent(), 'Roles');
|
||||
$editor_pos = strpos($this->getRawContent(), 'Text editor');
|
||||
$filters_pos = strpos($this->getRawContent(), 'Enabled filters');
|
||||
$this->assertTrue($roles_pos < $editor_pos && $editor_pos < $filters_pos, '"Text Editor" select appears in the correct location of the text format configuration UI.');
|
||||
|
||||
// Verify the <select>.
|
||||
$select = $this->xpath('//select[@name="editor[editor]"]');
|
||||
$select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]');
|
||||
$options = $this->xpath('//select[@name="editor[editor]"]/option');
|
||||
$this->assertTrue(count($select) === 1, 'The Text Editor select exists.');
|
||||
$this->assertTrue(count($select_is_disabled) === 1, 'The Text Editor select is disabled.');
|
||||
$this->assertTrue(count($options) === 1, 'The Text Editor select has only one option.');
|
||||
$this->assertTrue(((string) $options[0]) === 'None', 'Option 1 in the Text Editor select is "None".');
|
||||
$this->assertRaw(t('This option is disabled because no modules that provide a text editor are currently enabled.'), 'Description for select present that tells users to install a text editor module.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests adding a text editor to an existing text format.
|
||||
*/
|
||||
public function testAddEditorToExistingFormat() {
|
||||
$this->enableUnicornEditor();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/config/content/formats/manage/filtered_html');
|
||||
$edit = $this->selectUnicornEditor();
|
||||
// Configure Unicorn Editor's setting to another value.
|
||||
$edit['editor[settings][ponies_too]'] = FALSE;
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
$this->verifyUnicornEditorConfiguration('filtered_html', FALSE);
|
||||
|
||||
// Switch back to 'None' and check the Unicorn Editor's settings are gone.
|
||||
$edit = array(
|
||||
'editor[editor]' => '',
|
||||
);
|
||||
$this->drupalPostAjaxForm(NULL, $edit, 'editor_configure');
|
||||
$unicorn_setting = $this->xpath('//input[@name="editor[settings][ponies_too]" and @type="checkbox" and @checked]');
|
||||
$this->assertTrue(count($unicorn_setting) === 0, "Unicorn Editor's settings form is no longer present.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests adding a text editor to a new text format.
|
||||
*/
|
||||
public function testAddEditorToNewFormat() {
|
||||
$this->addEditorToNewFormat('monocerus', 'Monocerus');
|
||||
$this->verifyUnicornEditorConfiguration('monocerus');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests format disabling.
|
||||
*/
|
||||
public function testDisableFormatWithEditor() {
|
||||
$formats = ['monocerus' => 'Monocerus', 'tattoo' => 'Tattoo'];
|
||||
|
||||
// Install the node module.
|
||||
$this->container->get('module_installer')->install(['node']);
|
||||
$this->resetAll();
|
||||
// Create a new node type and attach the 'body' field to it.
|
||||
$node_type = NodeType::create(['type' => Unicode::strtolower($this->randomMachineName())]);
|
||||
$node_type->save();
|
||||
node_add_body_field($node_type, $this->randomString());
|
||||
|
||||
$permissions = ['administer filters', "edit any {$node_type->id()} content"];
|
||||
foreach ($formats as $format => $name) {
|
||||
// Create a format and add an editor to this format.
|
||||
$this->addEditorToNewFormat($format, $name);
|
||||
// Add permission for this format.
|
||||
$permissions[] = "use text format $format";
|
||||
}
|
||||
|
||||
// Create a node having the body format value 'moncerus'.
|
||||
$node = Node::create([
|
||||
'type' => $node_type->id(),
|
||||
'title' => $this->randomString(),
|
||||
]);
|
||||
$node->body->value = $this->randomString(100);
|
||||
$node->body->format = 'monocerus';
|
||||
$node->save();
|
||||
|
||||
// Log in as an user able to use both formats and edit nodes of created type.
|
||||
$account = $this->drupalCreateUser($permissions);
|
||||
$this->drupalLogin($account);
|
||||
|
||||
// The node edit page header.
|
||||
$text = t('<em>Edit @type</em> @title', array('@type' => $node_type->label(), '@title' => $node->label()));
|
||||
|
||||
// Go to node edit form.
|
||||
$this->drupalGet('node/' . $node->id() . '/edit');
|
||||
$this->assertRaw($text);
|
||||
|
||||
// Disable the format assigned to the 'body' field of the node.
|
||||
FilterFormat::load('monocerus')->disable()->save();
|
||||
|
||||
// Edit again the node.
|
||||
$this->drupalGet('node/' . $node->id() . '/edit');
|
||||
$this->assertRaw($text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an editor to a new format using the UI.
|
||||
*
|
||||
* @param string $format_id
|
||||
* The format id.
|
||||
* @param string $format_name
|
||||
* The format name.
|
||||
*/
|
||||
protected function addEditorToNewFormat($format_id, $format_name) {
|
||||
$this->enableUnicornEditor();
|
||||
$this->drupalLogin($this->adminUser);
|
||||
$this->drupalGet('admin/config/content/formats/add');
|
||||
// Configure the text format name.
|
||||
$edit = array(
|
||||
'name' => $format_name,
|
||||
'format' => $format_id,
|
||||
);
|
||||
$edit += $this->selectUnicornEditor();
|
||||
$this->drupalPostForm(NULL, $edit, t('Save configuration'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the unicorn editor.
|
||||
*/
|
||||
protected function enableUnicornEditor() {
|
||||
if (!$this->container->get('module_handler')->moduleExists('editor_test')) {
|
||||
$this->container->get('module_installer')->install(array('editor_test'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests and selects the unicorn editor.
|
||||
*
|
||||
* @return array
|
||||
* Returns an edit array containing the values to be posted.
|
||||
*/
|
||||
protected function selectUnicornEditor() {
|
||||
// Verify the <select> when a text editor is available.
|
||||
$select = $this->xpath('//select[@name="editor[editor]"]');
|
||||
$select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]');
|
||||
$options = $this->xpath('//select[@name="editor[editor]"]/option');
|
||||
$this->assertTrue(count($select) === 1, 'The Text Editor select exists.');
|
||||
$this->assertTrue(count($select_is_disabled) === 0, 'The Text Editor select is not disabled.');
|
||||
$this->assertTrue(count($options) === 2, 'The Text Editor select has two options.');
|
||||
$this->assertTrue(((string) $options[0]) === 'None', 'Option 1 in the Text Editor select is "None".');
|
||||
$this->assertTrue(((string) $options[1]) === 'Unicorn Editor', 'Option 2 in the Text Editor select is "Unicorn Editor".');
|
||||
$this->assertTrue(((string) $options[0]['selected']) === 'selected', 'Option 1 ("None") is selected.');
|
||||
// Ensure the none option is selected.
|
||||
$this->assertNoRaw(t('This option is disabled because no modules that provide a text editor are currently enabled.'), 'Description for select absent that tells users to install a text editor module.');
|
||||
|
||||
// Select the "Unicorn Editor" editor and click the "Configure" button.
|
||||
$edit = array(
|
||||
'editor[editor]' => 'unicorn',
|
||||
);
|
||||
$this->drupalPostAjaxForm(NULL, $edit, 'editor_configure');
|
||||
$unicorn_setting = $this->xpath('//input[@name="editor[settings][ponies_too]" and @type="checkbox" and @checked]');
|
||||
$this->assertTrue(count($unicorn_setting), "Unicorn Editor's settings form is present.");
|
||||
|
||||
return $edit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies unicorn editor configuration.
|
||||
*
|
||||
* @param string $format_id
|
||||
* The format machine name.
|
||||
* @param bool $ponies_too
|
||||
* The expected value of the ponies_too setting.
|
||||
*/
|
||||
protected function verifyUnicornEditorConfiguration($format_id, $ponies_too = TRUE) {
|
||||
$editor = editor_load($format_id);
|
||||
$settings = $editor->getSettings();
|
||||
$this->assertIdentical($editor->getEditor(), 'unicorn', 'The text editor is configured correctly.');
|
||||
$this->assertIdentical($settings['ponies_too'], $ponies_too, 'The text editor settings are stored correctly.');
|
||||
$this->drupalGet('admin/config/content/formats/manage/' . $format_id);
|
||||
$select = $this->xpath('//select[@name="editor[editor]"]');
|
||||
$select_is_disabled = $this->xpath('//select[@name="editor[editor]" and @disabled="disabled"]');
|
||||
$options = $this->xpath('//select[@name="editor[editor]"]/option');
|
||||
$this->assertTrue(count($select) === 1, 'The Text Editor select exists.');
|
||||
$this->assertTrue(count($select_is_disabled) === 0, 'The Text Editor select is not disabled.');
|
||||
$this->assertTrue(count($options) === 2, 'The Text Editor select has two options.');
|
||||
$this->assertTrue(((string) $options[1]['selected']) === 'selected', 'Option 2 ("Unicorn Editor") is selected.');
|
||||
}
|
||||
|
||||
}
|
||||
83
web/core/modules/editor/src/Tests/EditorDialogAccessTest.php
Normal file
83
web/core/modules/editor/src/Tests/EditorDialogAccessTest.php
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
|
||||
/**
|
||||
* Test access to the editor dialog forms.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorDialogAccessTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to install.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['editor', 'filter', 'ckeditor'];
|
||||
|
||||
/**
|
||||
* Test access to the editor image dialog.
|
||||
*/
|
||||
public function testEditorImageDialogAccess() {
|
||||
$url = Url::fromRoute('editor.image_dialog', ['editor' => 'plain_text']);
|
||||
$session = $this->assertSession();
|
||||
|
||||
// With no text editor, expect a 404.
|
||||
$this->drupalGet($url);
|
||||
$session->statusCodeEquals(404);
|
||||
|
||||
// With a text editor but without image upload settings, expect a 200, but
|
||||
// there should not be an input[type=file].
|
||||
$editor = Editor::create([
|
||||
'editor' => 'ckeditor',
|
||||
'format' => 'plain_text',
|
||||
'settings' => [
|
||||
'toolbar' => [
|
||||
'rows' => [
|
||||
[
|
||||
[
|
||||
'name' => 'Media',
|
||||
'items' => [
|
||||
'DrupalImage',
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
'plugins' => [],
|
||||
],
|
||||
'image_upload' => [
|
||||
'status' => FALSE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => '',
|
||||
'max_dimensions' => [
|
||||
'width' => 0,
|
||||
'height' => 0,
|
||||
],
|
||||
],
|
||||
]);
|
||||
$editor->save();
|
||||
$this->resetAll();
|
||||
$this->drupalGet($url);
|
||||
$this->assertTrue($this->cssSelect('input[type=text][name="attributes[src]"]'), 'Image uploads disabled: input[type=text][name="attributes[src]"] is present.');
|
||||
$this->assertFalse($this->cssSelect('input[type=file]'), 'Image uploads disabled: input[type=file] is absent.');
|
||||
$session->statusCodeEquals(200);
|
||||
|
||||
// With image upload settings, expect a 200, and now there should be an
|
||||
// input[type=file].
|
||||
$editor->setImageUploadSettings(['status' => TRUE] + $editor->getImageUploadSettings())
|
||||
->save();
|
||||
$this->resetAll();
|
||||
$this->drupalGet($url);
|
||||
$this->assertFalse($this->cssSelect('input[type=text][name="attributes[src]"]'), 'Image uploads enabled: input[type=text][name="attributes[src]"] is absent.');
|
||||
$this->assertTrue($this->cssSelect('input[type=file]'), 'Image uploads enabled: input[type=file] is present.');
|
||||
$session->statusCodeEquals(200);
|
||||
}
|
||||
|
||||
}
|
||||
285
web/core/modules/editor/src/Tests/EditorLoadingTest.php
Normal file
285
web/core/modules/editor/src/Tests/EditorLoadingTest.php
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
use Drupal\field\Entity\FieldStorageConfig;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* Tests loading of text editors.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorLoadingTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('filter', 'editor', 'editor_test', 'node');
|
||||
|
||||
/**
|
||||
* An untrusted user, with access to the 'plain_text' format.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $untrustedUser;
|
||||
|
||||
/**
|
||||
* A normal user with additional access to the 'filtered_html' format.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $normalUser;
|
||||
|
||||
/**
|
||||
* A privileged user with additional access to the 'full_html' format.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $privilegedUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Let there be T-rex.
|
||||
\Drupal::state()->set('editor_test_give_me_a_trex_thanks', TRUE);
|
||||
\Drupal::service('plugin.manager.editor')->clearCachedDefinitions();
|
||||
|
||||
// Add text formats.
|
||||
$filtered_html_format = FilterFormat::create(array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
$full_html_format = FilterFormat::create(array(
|
||||
'format' => 'full_html',
|
||||
'name' => 'Full HTML',
|
||||
'weight' => 1,
|
||||
'filters' => array(),
|
||||
));
|
||||
$full_html_format->save();
|
||||
|
||||
// Create article node type.
|
||||
$this->drupalCreateContentType(array(
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
));
|
||||
|
||||
// Create page node type, but remove the body.
|
||||
$this->drupalCreateContentType(array(
|
||||
'type' => 'page',
|
||||
'name' => 'Page',
|
||||
));
|
||||
$body = FieldConfig::loadByName('node', 'page', 'body');
|
||||
$body->delete();
|
||||
|
||||
// Create a formatted text field, which uses an <input type="text">.
|
||||
FieldStorageConfig::create(array(
|
||||
'field_name' => 'field_text',
|
||||
'entity_type' => 'node',
|
||||
'type' => 'text',
|
||||
))->save();
|
||||
|
||||
FieldConfig::create(array(
|
||||
'field_name' => 'field_text',
|
||||
'entity_type' => 'node',
|
||||
'label' => 'Textfield',
|
||||
'bundle' => 'page',
|
||||
))->save();
|
||||
|
||||
entity_get_form_display('node', 'page', 'default')
|
||||
->setComponent('field_text')
|
||||
->save();
|
||||
|
||||
// Create 3 users, each with access to different text formats.
|
||||
$this->untrustedUser = $this->drupalCreateUser(array('create article content', 'edit any article content'));
|
||||
$this->normalUser = $this->drupalCreateUser(array('create article content', 'edit any article content', 'use text format filtered_html'));
|
||||
$this->privilegedUser = $this->drupalCreateUser(array('create article content', 'edit any article content', 'create page content', 'edit any page content', 'use text format filtered_html', 'use text format full_html'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests loading of text editors.
|
||||
*/
|
||||
public function testLoading() {
|
||||
// Only associate a text editor with the "Full HTML" text format.
|
||||
$editor = Editor::create([
|
||||
'format' => 'full_html',
|
||||
'editor' => 'unicorn',
|
||||
'image_upload' => array(
|
||||
'status' => FALSE,
|
||||
'scheme' => file_default_scheme(),
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => '',
|
||||
'max_dimensions' => array('width' => '', 'height' => ''),
|
||||
)
|
||||
]);
|
||||
$editor->save();
|
||||
|
||||
// The normal user:
|
||||
// - has access to 2 text formats;
|
||||
// - doesn't have access to the full_html text format, so: no text editor.
|
||||
$this->drupalLogin($this->normalUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
list( , $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$this->assertFalse($editor_settings_present, 'No Text Editor module settings.');
|
||||
$this->assertFalse($editor_js_present, 'No Text Editor JavaScript.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
$this->assertTrue(count($format_selector) === 0, 'No text format selector exists on the page because the user only has access to a single format.');
|
||||
$this->drupalLogout($this->normalUser);
|
||||
|
||||
// The privileged user:
|
||||
// - has access to 2 text formats (and the fallback format);
|
||||
// - does have access to the full_html text format, so: Unicorn text editor.
|
||||
$this->drupalLogin($this->privilegedUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$expected = array('formats' => array('full_html' => array(
|
||||
'format' => 'full_html',
|
||||
'editor' => 'unicorn',
|
||||
'editorSettings' => array('ponyModeEnabled' => TRUE),
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
)));
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertIdentical($expected, $settings['editor'], "Text Editor module's JavaScript settings on the page are correct.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
$this->assertTrue(count($format_selector) === 1, 'A single text format selector exists on the page.');
|
||||
$specific_format_selector = $this->xpath('//select[contains(@class, "filter-list") and @data-editor-for="edit-body-0-value"]');
|
||||
$this->assertTrue(count($specific_format_selector) === 1, 'A single text format selector exists on the page and has a "data-editor-for" attribute with the correct value.');
|
||||
|
||||
// Load the editor image dialog form and make sure it does not fatal.
|
||||
$this->drupalGet('editor/dialog/image/full_html');
|
||||
$this->assertResponse(200);
|
||||
|
||||
$this->drupalLogout($this->privilegedUser);
|
||||
|
||||
// Also associate a text editor with the "Plain Text" text format.
|
||||
$editor = Editor::create([
|
||||
'format' => 'plain_text',
|
||||
'editor' => 'unicorn',
|
||||
]);
|
||||
$editor->save();
|
||||
|
||||
// The untrusted user:
|
||||
// - has access to 1 text format (plain_text);
|
||||
// - has access to the plain_text text format, so: Unicorn text editor.
|
||||
$this->drupalLogin($this->untrustedUser);
|
||||
$this->drupalGet('node/add/article');
|
||||
list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$expected = array('formats' => array('plain_text' => array(
|
||||
'format' => 'plain_text',
|
||||
'editor' => 'unicorn',
|
||||
'editorSettings' => array('ponyModeEnabled' => TRUE),
|
||||
'editorSupportsContentFiltering' => TRUE,
|
||||
'isXssSafe' => FALSE,
|
||||
)));
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertIdentical($expected, $settings['editor'], "Text Editor module's JavaScript settings on the page are correct.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
$this->assertTrue(count($format_selector) === 0, 'No text format selector exists on the page.');
|
||||
$hidden_input = $this->xpath('//input[@type="hidden" and @value="plain_text" and @data-editor-for="edit-body-0-value"]');
|
||||
$this->assertTrue(count($hidden_input) === 1, 'A single text format hidden input exists on the page and has a "data-editor-for" attribute with the correct value.');
|
||||
|
||||
// Create an "article" node that uses the full_html text format, then try
|
||||
// to let the untrusted user edit it.
|
||||
$this->drupalCreateNode(array(
|
||||
'type' => 'article',
|
||||
'body' => array(
|
||||
array('value' => $this->randomMachineName(32), 'format' => 'full_html')
|
||||
),
|
||||
));
|
||||
|
||||
// The untrusted user tries to edit content that is written in a text format
|
||||
// that (s)he is not allowed to use. The editor is still loaded. CKEditor,
|
||||
// for example, supports being loaded in a disabled state.
|
||||
$this->drupalGet('node/1/edit');
|
||||
list( , $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck('body');
|
||||
$this->assertTrue($editor_settings_present, 'Text Editor module settings.');
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript.');
|
||||
$this->assertTrue(count($body) === 1, 'A body field exists.');
|
||||
$this->assertFieldByXPath('//textarea[@id="edit-body-0-value" and @disabled="disabled"]', t('This field has been disabled because you do not have sufficient permissions to edit it.'), 'Text format access denied message found.');
|
||||
$this->assertTrue(count($format_selector) === 0, 'No text format selector exists on the page.');
|
||||
$hidden_input = $this->xpath('//input[@type="hidden" and contains(@class, "editor")]');
|
||||
$this->assertTrue(count($hidden_input) === 0, 'A single text format hidden input does not exist on the page.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Test supported element types.
|
||||
*/
|
||||
public function testSupportedElementTypes() {
|
||||
// Associate the unicorn text editor with the "Full HTML" text format.
|
||||
$editor = Editor::create([
|
||||
'format' => 'full_html',
|
||||
'editor' => 'unicorn',
|
||||
'image_upload' => array(
|
||||
'status' => FALSE,
|
||||
'scheme' => file_default_scheme(),
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => '',
|
||||
'max_dimensions' => array('width' => '', 'height' => ''),
|
||||
)
|
||||
]);
|
||||
$editor->save();
|
||||
|
||||
// Create an "page" node that uses the full_html text format.
|
||||
$this->drupalCreateNode(array(
|
||||
'type' => 'page',
|
||||
'field_text' => array(
|
||||
array('value' => $this->randomMachineName(32), 'format' => 'full_html')
|
||||
),
|
||||
));
|
||||
|
||||
// Assert the unicorn editor works with textfields.
|
||||
$this->drupalLogin($this->privilegedUser);
|
||||
$this->drupalGet('node/1/edit');
|
||||
list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
|
||||
$this->assertTrue($editor_settings_present, "Text Editor module's JavaScript settings are on the page.");
|
||||
$this->assertTrue($editor_js_present, 'Text Editor JavaScript is present.');
|
||||
$this->assertTrue(count($field) === 1, 'A text field exists.');
|
||||
$this->assertTrue(count($format_selector) === 1, 'A single text format selector exists on the page.');
|
||||
$specific_format_selector = $this->xpath('//select[contains(@class, "filter-list") and contains(@class, "editor") and @data-editor-for="edit-field-text-0-value"]');
|
||||
$this->assertTrue(count($specific_format_selector) === 1, 'A single text format selector exists on the page and has the "editor" class and a "data-editor-for" attribute with the correct value.');
|
||||
|
||||
// Associate the trex text editor with the "Full HTML" text format.
|
||||
$editor->delete();
|
||||
Editor::create([
|
||||
'format' => 'full_html',
|
||||
'editor' => 'trex',
|
||||
])->save();
|
||||
|
||||
$this->drupalGet('node/1/edit');
|
||||
list( , $editor_settings_present, $editor_js_present, $field, $format_selector) = $this->getThingsToCheck('field-text', 'input');
|
||||
$this->assertFalse($editor_settings_present, "Text Editor module's JavaScript settings are not on the page.");
|
||||
$this->assertFalse($editor_js_present, 'Text Editor JavaScript is not present.');
|
||||
$this->assertTrue(count($field) === 1, 'A text field exists.');
|
||||
$this->assertTrue(count($format_selector) === 1, 'A single text format selector exists on the page.');
|
||||
$specific_format_selector = $this->xpath('//select[contains(@class, "filter-list") and contains(@class, "editor") and @data-editor-for="edit-field-text-0-value"]');
|
||||
$this->assertFalse(count($specific_format_selector) === 1, 'A single text format selector exists on the page and has the "editor" class and a "data-editor-for" attribute with the correct value.');
|
||||
}
|
||||
|
||||
protected function getThingsToCheck($field_name, $type = 'textarea') {
|
||||
$settings = $this->getDrupalSettings();
|
||||
return array(
|
||||
// JavaScript settings.
|
||||
$settings,
|
||||
// Editor.module's JS settings present.
|
||||
isset($settings['editor']),
|
||||
// Editor.module's JS present.
|
||||
strpos($this->getRawContent(), drupal_get_path('module', 'editor') . '/js/editor.js') !== FALSE,
|
||||
// Body field.
|
||||
$this->xpath('//' . $type . '[@id="edit-' . $field_name . '-0-value"]'),
|
||||
// Format selector.
|
||||
$this->xpath('//select[contains(@class, "filter-list")]'),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\Tests\BrowserTestBase;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\user\RoleInterface;
|
||||
|
||||
/**
|
||||
* Tests Editor module's file reference filter with private files.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorPrivateFileReferenceFilterTest extends BrowserTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = [
|
||||
// Needed for the config: this is the only module in core that utilizes the
|
||||
// functionality in editor.module to be tested, and depends on that.
|
||||
'ckeditor',
|
||||
// Depends on filter.module (indirectly).
|
||||
'node',
|
||||
// Pulls in the config we're using during testing which create a text format
|
||||
// - with the filter_html_image_secure filter DISABLED,
|
||||
// - with the editor set to CKEditor,
|
||||
// - with drupalimage.image_upload.scheme set to 'private',
|
||||
// - with drupalimage.image_upload.directory set to ''.
|
||||
'editor_private_test',
|
||||
];
|
||||
|
||||
/**
|
||||
* Tests the editor file reference filter with private files.
|
||||
*/
|
||||
function testEditorPrivateFileReferenceFilter() {
|
||||
$author = $this->drupalCreateUser();
|
||||
$this->drupalLogin($author);
|
||||
|
||||
// Create a content type with a body field.
|
||||
$this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']);
|
||||
|
||||
// Create a file in the 'private:// ' stream.
|
||||
$filename = 'test.png';
|
||||
$src = '/system/files/' . $filename;
|
||||
/** @var \Drupal\file\FileInterface $file */
|
||||
$file = File::create([
|
||||
'uri' => 'private://' . $filename,
|
||||
]);
|
||||
$file->setTemporary();
|
||||
$file->setOwner($author);
|
||||
// Create the file itself.
|
||||
file_put_contents($file->getFileUri(), $this->randomString());
|
||||
$file->save();
|
||||
|
||||
// The image should be visible for its author.
|
||||
$this->drupalGet($src);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
// The not-yet-permanent image should NOT be visible for anonymous.
|
||||
$this->drupalLogout();
|
||||
$this->drupalGet($src);
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
|
||||
// Resave the file to be permanent.
|
||||
$file->setPermanent();
|
||||
$file->save();
|
||||
|
||||
// Create a node with its body field properly pointing to the just-created
|
||||
// file.
|
||||
$node = $this->drupalCreateNode([
|
||||
'type' => 'page',
|
||||
'body' => [
|
||||
'value' => '<img alt="alt" data-entity-type="file" data-entity-uuid="' . $file->uuid() . '" src="' . $src . '" />',
|
||||
'format' => 'private_images',
|
||||
],
|
||||
'uid' => $author->id(),
|
||||
]);
|
||||
|
||||
// Do the actual test. The image should be visible for anonymous users,
|
||||
// because they can view the referencing entity.
|
||||
$this->drupalGet($node->toUrl());
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
$this->drupalGet($src);
|
||||
$this->assertSession()->statusCodeEquals(200);
|
||||
|
||||
// Disallow anonymous users to view the entity, which then should also
|
||||
// disallow them to view the image.
|
||||
Role::load(RoleInterface::ANONYMOUS_ID)
|
||||
->revokePermission('access content')
|
||||
->save();
|
||||
$this->drupalGet($node->toUrl());
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
$this->drupalGet($src);
|
||||
$this->assertSession()->statusCodeEquals(403);
|
||||
}
|
||||
|
||||
}
|
||||
435
web/core/modules/editor/src/Tests/EditorSecurityTest.php
Normal file
435
web/core/modules/editor/src/Tests/EditorSecurityTest.php
Normal file
|
|
@ -0,0 +1,435 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* Tests XSS protection for content creators when using text editors.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorSecurityTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* The sample content to use in all tests.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $sampleContent = '<p style="color: red">Hello, Dumbo Octopus!</p><script>alert(0)</script><embed type="image/svg+xml" src="image.svg" />';
|
||||
|
||||
/**
|
||||
* The secured sample content to use in most tests.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $sampleContentSecured = '<p>Hello, Dumbo Octopus!</p>alert(0)';
|
||||
|
||||
/**
|
||||
* The secured sample content to use in tests when the <embed> tag is allowed.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected static $sampleContentSecuredEmbedAllowed = '<p>Hello, Dumbo Octopus!</p>alert(0)<embed type="image/svg+xml" src="image.svg" />';
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('filter', 'editor', 'editor_test', 'node');
|
||||
|
||||
/**
|
||||
* User with access to Restricted HTML text format without text editor.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $untrustedUser;
|
||||
|
||||
/**
|
||||
* User with access to Restricted HTML text format with text editor.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $normalUser;
|
||||
|
||||
/**
|
||||
* User with access to Restricted HTML text format, dangerous tags allowed
|
||||
* with text editor.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $trustedUser;
|
||||
|
||||
/**
|
||||
* User with access to all text formats and text editors.
|
||||
*
|
||||
* @var \Drupal\user\UserInterface
|
||||
*/
|
||||
protected $privilegedUser;
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create 5 text formats, to cover all potential use cases:
|
||||
// 1. restricted_without_editor (untrusted: anonymous)
|
||||
// 2. restricted_with_editor (normal: authenticated)
|
||||
// 3. restricted_plus_dangerous_tag_with_editor (privileged: trusted)
|
||||
// 4. unrestricted_without_editor (privileged: admin)
|
||||
// 5. unrestricted_with_editor (privileged: admin)
|
||||
// With text formats 2, 3 and 5, we also associate a text editor that does
|
||||
// not guarantee XSS safety. "restricted" means the text format has XSS
|
||||
// filters on output, "unrestricted" means the opposite.
|
||||
$format = FilterFormat::create(array(
|
||||
'format' => 'restricted_without_editor',
|
||||
'name' => 'Restricted HTML, without text editor',
|
||||
'weight' => 0,
|
||||
'filters' => array(
|
||||
// A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
|
||||
'filter_html' => array(
|
||||
'status' => 1,
|
||||
'settings' => array(
|
||||
'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
|
||||
)
|
||||
),
|
||||
),
|
||||
));
|
||||
$format->save();
|
||||
$format = FilterFormat::create(array(
|
||||
'format' => 'restricted_with_editor',
|
||||
'name' => 'Restricted HTML, with text editor',
|
||||
'weight' => 1,
|
||||
'filters' => array(
|
||||
// A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
|
||||
'filter_html' => array(
|
||||
'status' => 1,
|
||||
'settings' => array(
|
||||
'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a>',
|
||||
)
|
||||
),
|
||||
),
|
||||
));
|
||||
$format->save();
|
||||
$editor = Editor::create([
|
||||
'format' => 'restricted_with_editor',
|
||||
'editor' => 'unicorn',
|
||||
]);
|
||||
$editor->save();
|
||||
$format = FilterFormat::create(array(
|
||||
'format' => 'restricted_plus_dangerous_tag_with_editor',
|
||||
'name' => 'Restricted HTML, dangerous tag allowed, with text editor',
|
||||
'weight' => 1,
|
||||
'filters' => array(
|
||||
// A filter of the FilterInterface::TYPE_HTML_RESTRICTOR type.
|
||||
'filter_html' => array(
|
||||
'status' => 1,
|
||||
'settings' => array(
|
||||
'allowed_html' => '<h2> <h3> <h4> <h5> <h6> <p> <br> <strong> <a> <embed>',
|
||||
)
|
||||
),
|
||||
),
|
||||
));
|
||||
$format->save();
|
||||
$editor = Editor::create([
|
||||
'format' => 'restricted_plus_dangerous_tag_with_editor',
|
||||
'editor' => 'unicorn',
|
||||
]);
|
||||
$editor->save();
|
||||
$format = FilterFormat::create(array(
|
||||
'format' => 'unrestricted_without_editor',
|
||||
'name' => 'Unrestricted HTML, without text editor',
|
||||
'weight' => 0,
|
||||
'filters' => array(),
|
||||
));
|
||||
$format->save();
|
||||
$format = FilterFormat::create(array(
|
||||
'format' => 'unrestricted_with_editor',
|
||||
'name' => 'Unrestricted HTML, with text editor',
|
||||
'weight' => 1,
|
||||
'filters' => array(),
|
||||
));
|
||||
$format->save();
|
||||
$editor = Editor::create([
|
||||
'format' => 'unrestricted_with_editor',
|
||||
'editor' => 'unicorn',
|
||||
]);
|
||||
$editor->save();
|
||||
|
||||
|
||||
// Create node type.
|
||||
$this->drupalCreateContentType(array(
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
));
|
||||
|
||||
// Create 4 users, each with access to different text formats/editors:
|
||||
// - "untrusted": restricted_without_editor
|
||||
// - "normal": restricted_with_editor,
|
||||
// - "trusted": restricted_plus_dangerous_tag_with_editor
|
||||
// - "privileged": restricted_without_editor, restricted_with_editor,
|
||||
// restricted_plus_dangerous_tag_with_editor,
|
||||
// unrestricted_without_editor and unrestricted_with_editor
|
||||
$this->untrustedUser = $this->drupalCreateUser(array(
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'use text format restricted_without_editor',
|
||||
));
|
||||
$this->normalUser = $this->drupalCreateUser(array(
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'use text format restricted_with_editor',
|
||||
));
|
||||
$this->trustedUser = $this->drupalCreateUser(array(
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'use text format restricted_plus_dangerous_tag_with_editor',
|
||||
));
|
||||
$this->privilegedUser = $this->drupalCreateUser(array(
|
||||
'create article content',
|
||||
'edit any article content',
|
||||
'use text format restricted_without_editor',
|
||||
'use text format restricted_with_editor',
|
||||
'use text format restricted_plus_dangerous_tag_with_editor',
|
||||
'use text format unrestricted_without_editor',
|
||||
'use text format unrestricted_with_editor',
|
||||
));
|
||||
|
||||
// Create an "article" node for each possible text format, with the same
|
||||
// sample content, to do our tests on.
|
||||
$samples = array(
|
||||
array('author' => $this->untrustedUser->id(), 'format' => 'restricted_without_editor'),
|
||||
array('author' => $this->normalUser->id(), 'format' => 'restricted_with_editor'),
|
||||
array('author' => $this->trustedUser->id(), 'format' => 'restricted_plus_dangerous_tag_with_editor'),
|
||||
array('author' => $this->privilegedUser->id(), 'format' => 'unrestricted_without_editor'),
|
||||
array('author' => $this->privilegedUser->id(), 'format' => 'unrestricted_with_editor'),
|
||||
);
|
||||
foreach ($samples as $sample) {
|
||||
$this->drupalCreateNode(array(
|
||||
'type' => 'article',
|
||||
'body' => array(
|
||||
array('value' => self::$sampleContent, 'format' => $sample['format'])
|
||||
),
|
||||
'uid' => $sample['author']
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests initial security: is the user safe without switching text formats?
|
||||
*
|
||||
* Tests 8 scenarios. Tests only with a text editor that is not XSS-safe.
|
||||
*/
|
||||
function testInitialSecurity() {
|
||||
$expected = array(
|
||||
array(
|
||||
'node_id' => 1,
|
||||
'format' => 'restricted_without_editor',
|
||||
// No text editor => no XSS filtering.
|
||||
'value' => self::$sampleContent,
|
||||
'users' => array(
|
||||
$this->untrustedUser,
|
||||
$this->privilegedUser,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 2,
|
||||
'format' => 'restricted_with_editor',
|
||||
// Text editor => XSS filtering.
|
||||
'value' => self::$sampleContentSecured,
|
||||
'users' => array(
|
||||
$this->normalUser,
|
||||
$this->privilegedUser,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 3,
|
||||
'format' => 'restricted_plus_dangerous_tag_with_editor',
|
||||
// Text editor => XSS filtering.
|
||||
'value' => self::$sampleContentSecuredEmbedAllowed,
|
||||
'users' => array(
|
||||
$this->trustedUser,
|
||||
$this->privilegedUser,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 4,
|
||||
'format' => 'unrestricted_without_editor',
|
||||
// No text editor => no XSS filtering.
|
||||
'value' => self::$sampleContent,
|
||||
'users' => array(
|
||||
$this->privilegedUser,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 5,
|
||||
'format' => 'unrestricted_with_editor',
|
||||
// Text editor, no security filter => no XSS filtering.
|
||||
'value' => self::$sampleContent,
|
||||
'users' => array(
|
||||
$this->privilegedUser,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Log in as each user that may edit the content, and assert the value.
|
||||
foreach ($expected as $case) {
|
||||
foreach ($case['users'] as $account) {
|
||||
$this->pass(format_string('Scenario: sample %sample_id, %format.', array(
|
||||
'%sample_id' => $case['node_id'],
|
||||
'%format' => $case['format'],
|
||||
)));
|
||||
$this->drupalLogin($account);
|
||||
$this->drupalGet('node/' . $case['node_id'] . '/edit');
|
||||
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
|
||||
$this->assertIdentical($case['value'], (string) $dom_node[0], 'The value was correctly filtered for XSS attack vectors.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests administrator security: is the user safe when switching text formats?
|
||||
*
|
||||
* Tests 24 scenarios. Tests only with a text editor that is not XSS-safe.
|
||||
*
|
||||
* When changing from a more restrictive text format with a text editor (or a
|
||||
* text format without a text editor) to a less restrictive text format, it is
|
||||
* possible that a malicious user could trigger an XSS.
|
||||
*
|
||||
* E.g. when switching a piece of text that uses the Restricted HTML text
|
||||
* format and contains a <script> tag to the Full HTML text format, the
|
||||
* <script> tag would be executed. Unless we apply appropriate filtering.
|
||||
*/
|
||||
function testSwitchingSecurity() {
|
||||
$expected = array(
|
||||
array(
|
||||
'node_id' => 1,
|
||||
'value' => self::$sampleContent, // No text editor => no XSS filtering.
|
||||
'format' => 'restricted_without_editor',
|
||||
'switch_to' => array(
|
||||
'restricted_with_editor' => self::$sampleContentSecured,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
|
||||
// No text editor => no XSS filtering.
|
||||
'unrestricted_without_editor' => FALSE,
|
||||
'unrestricted_with_editor' => self::$sampleContentSecured,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 2,
|
||||
'value' => self::$sampleContentSecured, // Text editor => XSS filtering.
|
||||
'format' => 'restricted_with_editor',
|
||||
'switch_to' => array(
|
||||
// No text editor => no XSS filtering.
|
||||
'restricted_without_editor' => FALSE,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
|
||||
// No text editor => no XSS filtering.
|
||||
'unrestricted_without_editor' => FALSE,
|
||||
'unrestricted_with_editor' => self::$sampleContentSecured,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 3,
|
||||
'value' => self::$sampleContentSecuredEmbedAllowed, // Text editor => XSS filtering.
|
||||
'format' => 'restricted_plus_dangerous_tag_with_editor',
|
||||
'switch_to' => array(
|
||||
// No text editor => no XSS filtering.
|
||||
'restricted_without_editor' => FALSE,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'restricted_with_editor' => self::$sampleContentSecured,
|
||||
// No text editor => no XSS filtering.
|
||||
'unrestricted_without_editor' => FALSE,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'unrestricted_with_editor' => self::$sampleContentSecured,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 4,
|
||||
'value' => self::$sampleContent, // No text editor => no XSS filtering.
|
||||
'format' => 'unrestricted_without_editor',
|
||||
'switch_to' => array(
|
||||
// No text editor => no XSS filtering.
|
||||
'restricted_without_editor' => FALSE,
|
||||
'restricted_with_editor' => self::$sampleContentSecured,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
|
||||
// From no editor, no security filters, to editor, still no security
|
||||
// filters: resulting content when viewed was already vulnerable, so
|
||||
// it must be intentional.
|
||||
'unrestricted_with_editor' => FALSE,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'node_id' => 5,
|
||||
'value' => self::$sampleContentSecured, // Text editor => XSS filtering.
|
||||
'format' => 'unrestricted_with_editor',
|
||||
'switch_to' => array(
|
||||
// From editor, no security filters to security filters, no editor: no
|
||||
// risk.
|
||||
'restricted_without_editor' => FALSE,
|
||||
'restricted_with_editor' => self::$sampleContentSecured,
|
||||
// Intersection of restrictions => most strict XSS filtering.
|
||||
'restricted_plus_dangerous_tag_with_editor' => self::$sampleContentSecured,
|
||||
// From no editor, no security filters, to editor, still no security
|
||||
// filters: resulting content when viewed was already vulnerable, so
|
||||
// it must be intentional.
|
||||
'unrestricted_without_editor' => FALSE,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// Log in as the privileged user, and for every sample, do the following:
|
||||
// - switch to every other text format/editor
|
||||
// - assert the XSS-filtered values that we get from the server
|
||||
$this->drupalLogin($this->privilegedUser);
|
||||
foreach ($expected as $case) {
|
||||
$this->drupalGet('node/' . $case['node_id'] . '/edit');
|
||||
|
||||
// Verify data- attributes.
|
||||
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
|
||||
$this->assertIdentical(self::$sampleContent, (string) $dom_node[0]['data-editor-value-original'], 'The data-editor-value-original attribute is correctly set.');
|
||||
$this->assertIdentical('false', (string) $dom_node[0]['data-editor-value-is-changed'], 'The data-editor-value-is-changed attribute is correctly set.');
|
||||
|
||||
// Switch to every other text format/editor and verify the results.
|
||||
foreach ($case['switch_to'] as $format => $expected_filtered_value) {
|
||||
$this->pass(format_string('Scenario: sample %sample_id, switch from %original_format to %format.', array(
|
||||
'%sample_id' => $case['node_id'],
|
||||
'%original_format' => $case['format'],
|
||||
'%format' => $format,
|
||||
)));
|
||||
$post = array(
|
||||
'value' => self::$sampleContent,
|
||||
'original_format_id' => $case['format'],
|
||||
);
|
||||
$response = $this->drupalPostWithFormat('editor/filter_xss/' . $format, 'json', $post);
|
||||
$this->assertResponse(200);
|
||||
$json = Json::decode($response);
|
||||
$this->assertIdentical($json, $expected_filtered_value, 'The value was correctly filtered for XSS attack vectors.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the standard text editor XSS filter being overridden.
|
||||
*/
|
||||
function testEditorXssFilterOverride() {
|
||||
// First: the Standard text editor XSS filter.
|
||||
$this->drupalLogin($this->normalUser);
|
||||
$this->drupalGet('node/2/edit');
|
||||
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
|
||||
$this->assertIdentical(self::$sampleContentSecured, (string) $dom_node[0], 'The value was filtered by the Standard text editor XSS filter.');
|
||||
|
||||
// Enable editor_test.module's hook_editor_xss_filter_alter() implementation
|
||||
// to alter the text editor XSS filter class being used.
|
||||
\Drupal::state()->set('editor_test_editor_xss_filter_alter_enabled', TRUE);
|
||||
|
||||
// First: the Insecure text editor XSS filter.
|
||||
$this->drupalGet('node/2/edit');
|
||||
$dom_node = $this->xpath('//textarea[@id="edit-body-0-value"]');
|
||||
$this->assertIdentical(self::$sampleContent, (string) $dom_node[0], 'The value was filtered by the Insecure text editor XSS filter.');
|
||||
}
|
||||
|
||||
}
|
||||
223
web/core/modules/editor/src/Tests/EditorUploadImageScaleTest.php
Normal file
223
web/core/modules/editor/src/Tests/EditorUploadImageScaleTest.php
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\editor\Entity\Editor;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
|
||||
/**
|
||||
* Tests scaling of inline images.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorUploadImageScaleTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = ['editor', 'editor_test'];
|
||||
|
||||
/**
|
||||
* A user with permission as administer for testing.
|
||||
*
|
||||
* @var \Drupal\user\Entity\User
|
||||
*/
|
||||
protected $adminUser;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Add text format.
|
||||
FilterFormat::create([
|
||||
'format' => 'basic_html',
|
||||
'name' => 'Basic HTML',
|
||||
'weight' => 0,
|
||||
])->save();
|
||||
|
||||
// Set up text editor.
|
||||
Editor::create([
|
||||
'format' => 'basic_html',
|
||||
'editor' => 'unicorn',
|
||||
'image_upload' => [
|
||||
'status' => TRUE,
|
||||
'scheme' => 'public',
|
||||
'directory' => 'inline-images',
|
||||
'max_size' => '',
|
||||
'max_dimensions' => [
|
||||
'width' => NULL,
|
||||
'height' => NULL
|
||||
],
|
||||
]
|
||||
])->save();
|
||||
|
||||
// Create admin user.
|
||||
$this->adminUser = $this->drupalCreateUser(['administer filters', 'use text format basic_html']);
|
||||
$this->drupalLogin($this->adminUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests scaling of inline images.
|
||||
*/
|
||||
public function testEditorUploadImageScale() {
|
||||
// Generate testing images.
|
||||
$testing_image_list = $this->drupalGetTestFiles('image');
|
||||
|
||||
// Case 1: no max dimensions set: uploaded image not scaled.
|
||||
$test_image = $testing_image_list[0];
|
||||
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
|
||||
$max_width = NULL;
|
||||
$max_height = NULL;
|
||||
$this->setMaxDimensions($max_width, $max_height);
|
||||
$this->assertSavedMaxDimensions($max_width, $max_height);
|
||||
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
|
||||
$this->assertEqual($uploaded_image_file_width, $image_file_width);
|
||||
$this->assertEqual($uploaded_image_file_height, $image_file_height);
|
||||
$this->assertNoRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
|
||||
|
||||
// Case 2: max width smaller than uploaded image: image scaled down.
|
||||
$test_image = $testing_image_list[1];
|
||||
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
|
||||
$max_width = $image_file_width - 5;
|
||||
$max_height = $image_file_height;
|
||||
$this->setMaxDimensions($max_width, $max_height);
|
||||
$this->assertSavedMaxDimensions($max_width, $max_height);
|
||||
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
|
||||
$this->assertEqual($uploaded_image_file_width, $max_width);
|
||||
$this->assertEqual($uploaded_image_file_height, $uploaded_image_file_height * ($uploaded_image_file_width / $max_width));
|
||||
$this->assertRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
|
||||
|
||||
// Case 3: max height smaller than uploaded image: image scaled down.
|
||||
$test_image = $testing_image_list[2];
|
||||
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
|
||||
$max_width = $image_file_width;
|
||||
$max_height = $image_file_height - 5;
|
||||
$this->setMaxDimensions($max_width, $max_height);
|
||||
$this->assertSavedMaxDimensions($max_width, $max_height);
|
||||
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
|
||||
$this->assertEqual($uploaded_image_file_width, $uploaded_image_file_width * ($uploaded_image_file_height / $max_height));
|
||||
$this->assertEqual($uploaded_image_file_height, $max_height);
|
||||
$this->assertRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
|
||||
|
||||
// Case 4: max dimensions greater than uploaded image: image not scaled.
|
||||
$test_image = $testing_image_list[3];
|
||||
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
|
||||
$max_width = $image_file_width + 5;
|
||||
$max_height = $image_file_height + 5;
|
||||
$this->setMaxDimensions($max_width, $max_height);
|
||||
$this->assertSavedMaxDimensions($max_width, $max_height);
|
||||
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
|
||||
$this->assertEqual($uploaded_image_file_width, $image_file_width);
|
||||
$this->assertEqual($uploaded_image_file_height, $image_file_height);
|
||||
$this->assertNoRaw(t('The image was resized to fit within the maximum allowed dimensions of %dimensions pixels.', ['%dimensions' => $max_width . 'x' . $max_height]));
|
||||
|
||||
// Case 5: only max width dimension was provided and it was smaller than
|
||||
// uploaded image: image scaled down.
|
||||
$test_image = $testing_image_list[4];
|
||||
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
|
||||
$max_width = $image_file_width - 5;
|
||||
$max_height = NULL;
|
||||
$this->setMaxDimensions($max_width, $max_height);
|
||||
$this->assertSavedMaxDimensions($max_width, $max_height);
|
||||
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
|
||||
$this->assertEqual($uploaded_image_file_width, $max_width);
|
||||
$this->assertEqual($uploaded_image_file_height, $uploaded_image_file_height * ($uploaded_image_file_width / $max_width));
|
||||
$this->assertRaw(t('The image was resized to fit within the maximum allowed width of %width pixels.', ['%width' => $max_width]));
|
||||
|
||||
// Case 6: only max height dimension was provided and it was smaller than
|
||||
// uploaded image: image scaled down.
|
||||
$test_image = $testing_image_list[5];
|
||||
list($image_file_width, $image_file_height) = $this->getTestImageInfo($test_image->uri);
|
||||
$max_width = NULL;
|
||||
$max_height = $image_file_height - 5;
|
||||
$this->setMaxDimensions($max_width, $max_height);
|
||||
$this->assertSavedMaxDimensions($max_width, $max_height);
|
||||
list($uploaded_image_file_width, $uploaded_image_file_height) = $this->uploadImage($test_image->uri);
|
||||
$this->assertEqual($uploaded_image_file_width, $uploaded_image_file_width * ($uploaded_image_file_height / $max_height));
|
||||
$this->assertEqual($uploaded_image_file_height, $max_height);
|
||||
$this->assertRaw(t('The image was resized to fit within the maximum allowed height of %height pixels.', ['%height' => $max_height]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the dimensions of an uploaded image.
|
||||
*
|
||||
* @param string $uri
|
||||
* The URI of the image.
|
||||
*
|
||||
* @return array
|
||||
* An array containing the uploaded image's width and height.
|
||||
*/
|
||||
protected function getTestImageInfo($uri) {
|
||||
$image_file = $this->container->get('image.factory')->get($uri);
|
||||
return [
|
||||
(int) $image_file->getWidth(),
|
||||
(int) $image_file->getHeight(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum dimensions and saves the configuration.
|
||||
*
|
||||
* @param string|int $width
|
||||
* The width of the image.
|
||||
* @param string|int $height
|
||||
* The height of the image.
|
||||
*/
|
||||
protected function setMaxDimensions($width, $height) {
|
||||
$editor = Editor::load('basic_html');
|
||||
$image_upload_settings = $editor->getImageUploadSettings();
|
||||
$image_upload_settings['max_dimensions']['width'] = $width;
|
||||
$image_upload_settings['max_dimensions']['height'] = $height;
|
||||
$editor->setImageUploadSettings($image_upload_settings);
|
||||
$editor->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads an image via the editor dialog.
|
||||
*
|
||||
* @param string $uri
|
||||
* The URI of the image.
|
||||
*
|
||||
* @return array
|
||||
* An array containing the uploaded image's width and height.
|
||||
*/
|
||||
protected function uploadImage($uri) {
|
||||
$edit = [
|
||||
'files[fid]' => drupal_realpath($uri),
|
||||
];
|
||||
$this->drupalGet('editor/dialog/image/basic_html');
|
||||
$this->drupalPostForm('editor/dialog/image/basic_html', $edit, t('Upload'));
|
||||
$uploaded_image_file = $this->container->get('image.factory')->get('public://inline-images/' . basename($uri));
|
||||
return [
|
||||
(int) $uploaded_image_file->getWidth(),
|
||||
(int) $uploaded_image_file->getHeight(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts whether the saved maximum dimensions equal the ones provided.
|
||||
*
|
||||
* @param string $width
|
||||
* The expected width of the uploaded image.
|
||||
* @param string $height
|
||||
* The expected height of the uploaded image.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function assertSavedMaxDimensions($width, $height) {
|
||||
$image_upload_settings = Editor::load('basic_html')->getImageUploadSettings();
|
||||
$expected = [
|
||||
'width' => $image_upload_settings['max_dimensions']['width'],
|
||||
'height' => $image_upload_settings['max_dimensions']['height'],
|
||||
];
|
||||
$same_width = $this->assertEqual($width, $expected['width'], 'Actual width of "' . $width . '" equals the expected width of "' . $expected['width'] . '"');
|
||||
$same_height = $this->assertEqual($height, $expected['height'], 'Actual height of "' . $height . '" equals the expected width of "' . $expected['height'] . '"');
|
||||
return $same_width && $same_height;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Tests;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\EventSubscriber\MainContentViewSubscriber;
|
||||
use Drupal\simpletest\WebTestBase;
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* Tests Quick Edit module integration endpoints.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class QuickEditIntegrationLoadingTest extends WebTestBase {
|
||||
|
||||
/**
|
||||
* Modules to enable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $modules = array('quickedit', 'filter', 'node', 'editor');
|
||||
|
||||
/**
|
||||
* The basic permissions necessary to view content and use in-place editing.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected static $basicPermissions = array('access content', 'create article content', 'use text format filtered_html', 'access contextual links');
|
||||
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
// Create a text format.
|
||||
$filtered_html_format = FilterFormat::create(array(
|
||||
'format' => 'filtered_html',
|
||||
'name' => 'Filtered HTML',
|
||||
'weight' => 0,
|
||||
'filters' => array(
|
||||
'filter_caption' => array(
|
||||
'status' => 1,
|
||||
),
|
||||
),
|
||||
));
|
||||
$filtered_html_format->save();
|
||||
|
||||
// Create a node type.
|
||||
$this->drupalCreateContentType(array(
|
||||
'type' => 'article',
|
||||
'name' => 'Article',
|
||||
));
|
||||
|
||||
// Create one node of the above node type using the above text format.
|
||||
$this->drupalCreateNode(array(
|
||||
'type' => 'article',
|
||||
'body' => array(
|
||||
0 => array(
|
||||
'value' => '<p>Do you also love Drupal?</p><img src="druplicon.png" data-caption="Druplicon" />',
|
||||
'format' => 'filtered_html',
|
||||
)
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Test loading of untransformed text when a user doesn't have access to it.
|
||||
*/
|
||||
public function testUsersWithoutPermission() {
|
||||
// Create 3 users, each with insufficient permissions, i.e. without either
|
||||
// or both of the following permissions:
|
||||
// - the 'access in-place editing' permission
|
||||
// - the 'edit any article content' permission (necessary to edit node 1)
|
||||
$users = array(
|
||||
$this->drupalCreateUser(static::$basicPermissions),
|
||||
$this->drupalCreateUser(array_merge(static::$basicPermissions, array('edit any article content'))),
|
||||
$this->drupalCreateUser(array_merge(static::$basicPermissions, array('access in-place editing')))
|
||||
);
|
||||
|
||||
// Now test with each of the 3 users with insufficient permissions.
|
||||
foreach ($users as $user) {
|
||||
$this->drupalLogin($user);
|
||||
$this->drupalGet('node/1');
|
||||
|
||||
// Ensure the text is transformed.
|
||||
$this->assertRaw('<p>Do you also love Drupal?</p><figure role="group" class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
|
||||
|
||||
// Retrieving the untransformed text should result in an empty 403 response.
|
||||
$response = $this->drupalPost('editor/' . 'node/1/body/en/full', '', array(), array('query' => array(MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax')));
|
||||
$this->assertResponse(403);
|
||||
$this->assertIdentical('{}', $response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test loading of untransformed text when a user does have access to it.
|
||||
*/
|
||||
public function testUserWithPermission() {
|
||||
$user = $this->drupalCreateUser(array_merge(static::$basicPermissions, array('edit any article content', 'access in-place editing')));
|
||||
$this->drupalLogin($user);
|
||||
$this->drupalGet('node/1');
|
||||
|
||||
// Ensure the text is transformed.
|
||||
$this->assertRaw('<p>Do you also love Drupal?</p><figure role="group" class="caption caption-img"><img src="druplicon.png" /><figcaption>Druplicon</figcaption></figure>');
|
||||
|
||||
$response = $this->drupalPost('editor/' . 'node/1/body/en/full', '', [], ['query' => [MainContentViewSubscriber::WRAPPER_FORMAT => 'drupal_ajax']]);
|
||||
$this->assertResponse(200);
|
||||
$ajax_commands = Json::decode($response);
|
||||
$this->assertIdentical(1, count($ajax_commands), 'The untransformed text POST request results in one AJAX command.');
|
||||
$this->assertIdentical('editorGetUntransformedText', $ajax_commands[0]['command'], 'The first AJAX command is an editorGetUntransformedText command.');
|
||||
$this->assertIdentical('<p>Do you also love Drupal?</p><img src="druplicon.png" data-caption="Druplicon" />', $ajax_commands[0]['data'], 'The editorGetUntransformedText command contains the expected data.');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\editor\Tests\Update;
|
||||
|
||||
use Drupal\system\Tests\Update\UpdatePathTestBase;
|
||||
|
||||
/**
|
||||
* Tests Editor module database updates.
|
||||
*
|
||||
* @group editor
|
||||
*/
|
||||
class EditorUpdateTest extends UpdatePathTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setDatabaseDumpFiles() {
|
||||
$this->databaseDumpFiles = [
|
||||
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.bare.standard.php.gz',
|
||||
// Simulate an un-synchronized environment.
|
||||
__DIR__ . '/../../../../system/tests/fixtures/update/drupal-8.editor-editor_update_8001.php',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests editor_update_8001().
|
||||
*
|
||||
* @see editor_update_8001()
|
||||
*/
|
||||
public function testEditorUpdate8001() {
|
||||
/** @var \Drupal\Core\Config\ConfigFactoryInterface $config_factory */
|
||||
$config_factory = $this->container->get('config.factory');
|
||||
|
||||
$format_basic_html = $config_factory->get('filter.format.basic_html');
|
||||
$editor_basic_html = $config_factory->get('editor.editor.basic_html');
|
||||
$format_full_html = $config_factory->get('filter.format.full_html');
|
||||
$editor_full_html = $config_factory->get('editor.editor.full_html');
|
||||
|
||||
// Checks if the 'basic_html' format and editor statuses differ.
|
||||
$this->assertTrue($format_basic_html->get('status'));
|
||||
$this->assertFalse($editor_basic_html->get('status'));
|
||||
$this->assertNotIdentical($format_basic_html->get('status'), $editor_basic_html->get('status'));
|
||||
|
||||
// Checks if the 'full_html' format and editor statuses differ.
|
||||
$this->assertFalse($format_full_html->get('status'));
|
||||
$this->assertTrue($editor_full_html->get('status'));
|
||||
$this->assertNotIdentical($format_full_html->get('status'), $editor_full_html->get('status'));
|
||||
|
||||
|
||||
// Run updates.
|
||||
$this->runUpdates();
|
||||
|
||||
// Reload text formats and editors.
|
||||
$format_basic_html = $config_factory->get('filter.format.basic_html');
|
||||
$editor_basic_html = $config_factory->get('editor.editor.basic_html');
|
||||
$format_full_html = $config_factory->get('filter.format.full_html');
|
||||
$editor_full_html = $config_factory->get('editor.editor.full_html');
|
||||
|
||||
// Checks if the 'basic_html' format and editor statuses are in sync.
|
||||
$this->assertTrue($format_basic_html->get('status'));
|
||||
$this->assertTrue($editor_basic_html->get('status'));
|
||||
$this->assertIdentical($format_basic_html->get('status'), $editor_basic_html->get('status'));
|
||||
|
||||
// Checks if the 'full_html' format and editor statuses are in sync.
|
||||
$this->assertFalse($format_full_html->get('status'));
|
||||
$this->assertFalse($editor_full_html->get('status'));
|
||||
$this->assertIdentical($format_full_html->get('status'), $editor_full_html->get('status'));
|
||||
}
|
||||
|
||||
}
|
||||
Reference in a new issue