diff --git a/core/modules/image/config/schema/image.schema.yml b/core/modules/image/config/schema/image.schema.yml index 924d454f91..97e107ca2f 100644 --- a/core/modules/image/config/schema/image.schema.yml +++ b/core/modules/image/config/schema/image.schema.yml @@ -157,3 +157,14 @@ field.widget.settings.image_image: preview_image_style: type: string label: 'Preview image style' + +filter_settings.filter_image_style: + type: filter + label: 'Display image styles' + mapping: + allowed_styles: + type: sequence + label: 'Allowed image styles' + sequence: + type: string + label: 'Image style ID' diff --git a/core/modules/image/image.module b/core/modules/image/image.module index 30358b9065..308ab15766 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -6,11 +6,14 @@ */ use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; -use Drupal\file\Entity\File; -use Drupal\field\FieldStorageConfigInterface; +use Drupal\Core\Url; use Drupal\field\FieldConfigInterface; +use Drupal\field\FieldStorageConfigInterface; +use Drupal\file\Entity\File; use Drupal\image\Entity\ImageStyle; +use Drupal\image\ImageStyleInterface; /** * Image style constant for user presets in the database. @@ -508,3 +511,112 @@ function image_field_config_delete(FieldConfigInterface $field) { \Drupal::service('file.usage')->delete($file, 'image', 'default_image', $field->uuid()); } } + +/** + * Implements hook_form_FORM_ID_alter(). + * + * Alters the image dialog form for text editor to allow the user to select an + * image style. + * + * @see \Drupal\editor\Form\EditorImageDialog::buildForm() + */ +function image_form_editor_image_dialog_alter(array &$form, FormStateInterface $form_state) { + /** @var \Drupal\editor\EditorInterface $editor */ + $editor = $form_state->getBuildInfo()['args'][0]; + + /** @var \Drupal\image\Plugin\Filter\FilterImageStyle $filter */ + $filter = $editor->getFilterFormat()->filters('filter_image_style'); + + $access_filter_status = $filter->status; + + // Get the image () that is being edited on the client. + $image_element = $form_state->get('image_element'); + + // Get an array of image styles to present as options for selection. + $image_style_options = array_map(function (ImageStyleInterface $image_style) { + return $image_style->label(); + }, $filter->getAllowedImageStyles()); + + // Add a select element to choose an image style. + $form['image_style']['selection'] = [ + '#title' => t('Use image style'), + '#type' => 'select', + '#default_value' => isset($image_element['data-image-style']) ? $image_element['data-image-style'] : NULL, + '#options' => $image_style_options, + '#empty_value' => '', + '#parents' => ['attributes', 'data-image-style'], + '#access' => $access_filter_status, + ]; + + // Check access to configure allowed image styles for the configurtation + // link. + $route_parameters = ['filter_format' => $editor->getFilterFormat()->id()]; + $access_manager = \Drupal::service('access_manager'); + $access_administer_styles = \Drupal::currentUser()->hasPermission('administer image styles'); + $access_filter_form = $access_manager->checkNamedRoute('entity.filter_format.edit_form', $route_parameters, \Drupal::currentUser()); + + // Add a link to the configuration of the image styles. + $url = Url::fromRoute('entity.filter_format.edit_form', $route_parameters); + $url->setOptions(['fragment' => 'edit-filters-filter-image-style-settings']); + $form['image_style']['link'] = [ + '#title' => t('Configure allowed image styles'), + '#type' => 'link', + '#url' => $url, + '#access' => $access_filter_status && $access_administer_styles && $access_filter_form, + '#attributes' => [ + 'target' => '_blank', + ], + ]; + + if ($access_filter_status) { + $form['actions']['save_modal']['#validate'][] = 'image_form_editor_image_dialog_validate'; + } +} + +/** + * Form validation handler for EditorImageDialog. + * + * Ensures the image shown in the text editor matches the chosen image style. + * + * @see \Drupal\editor\Form\EditorImageDialog::buildForm() + * @see \Drupal\editor\Form\EditorImageDialog::validateForm() + * @see image_form_editor_image_dialog_alter() + */ +function image_form_editor_image_dialog_validate(array &$form, FormStateInterface $form_state) { + if (!empty($form_state->getValue('fid')[0])) { + $attributes = &$form_state->getValue('attributes'); + + /** @var \Drupal\image\ImageStyleInterface $image_style */ + $image_style = ImageStyle::load($attributes['data-image-style']); + + if (!empty($image_style)) { + /** @var \Drupal\file\FileInterface $file */ + $file = File::load($form_state->getValue('fid')[0]); + + $uri = $file->getFileUri(); + + // Set the 'src' attribute to the image style URL. FilterImageStyle will + // look at the 'data-editor-file-uuid' attribute, not the 'src' attribute + // to render the appropriate output. + $image_url = $image_style->buildUrl($uri); + $attributes['src'] = file_url_transform_relative($image_url); + + /** @var \Drupal\Core\Image\ImageInterface $image */ + $image = \Drupal::service('image.factory')->get($uri); + + if ($image->isValid()) { + // Get the original width and height of the image. + $dimensions = [ + 'width' => $image->getWidth(), + 'height' => $image->getHeight(), + ]; + + // Transform the 'width' and 'height' dimensions of the image based on + // the image style. + $image_style->transformDimensions($dimensions, $attributes['src']); + $attributes['width'] = $dimensions['width']; + $attributes['height'] = $dimensions['height']; + } + } + } +} diff --git a/core/modules/image/image.post_update.php b/core/modules/image/image.post_update.php index 04d8c4b7b9..79ce7e52b8 100644 --- a/core/modules/image/image.post_update.php +++ b/core/modules/image/image.post_update.php @@ -7,6 +7,8 @@ use Drupal\Core\Entity\Entity\EntityViewDisplay; use Drupal\Core\Entity\Entity\EntityFormDisplay; +use Drupal\filter\Entity\FilterFormat; +use Drupal\filter\Plugin\FilterInterface; /** * Saves the image style dependencies into form and view display entities. @@ -20,3 +22,39 @@ function image_post_update_image_style_dependencies() { $display->save(); } } + +/** + * Update filter formats to allow the use of the image style filter. + */ +function image_post_update_enable_filter_image_style() { + /** @var \Drupal\filter\FilterFormatInterface[] $formats */ + $formats = FilterFormat::loadMultiple(); + foreach ($formats as $format) { + $changed = FALSE; + if (in_array($format->id(), ['basic_html', 'full_html'])) { + // Enable the image style filter, and set the weight to the highest + // current weight + 1 so that it appears last in the list. + $highest_weight = array_reduce($format->filters()->getAll(), function ($carry, FilterInterface $filter) { + return $filter->status !== FALSE && ($carry === NULL || $filter->weight > $carry) ? $filter->weight : $carry; + }); + $format->setFilterConfig('filter_image_style', ['status' => TRUE, 'weight' => $highest_weight + 1]); + $changed = TRUE; + } + // Update the allowed html tags of filter_html filter if it's enabled. + if ($filter = $format->filters('filter_html')) { + $config = $filter->getConfiguration(); + $allowed_html = !empty($config['settings']['allowed_html']) ? $config['settings']['allowed_html'] : NULL; + $matches = []; + if ($allowed_html && preg_match('/]*)>/', $allowed_html, $matches)) { + $attributes = array_filter(explode(' ', $matches[1])); + $attributes[] = 'data-image-style'; + $config['settings']['allowed_html'] = preg_replace('/]*)>/', '', $allowed_html); + $format->setFilterConfig('filter_html', $config); + $changed = TRUE; + } + } + if ($changed) { + $format->save(); + } + } +} diff --git a/core/modules/image/js/plugins/drupalimagestyle/plugin.js b/core/modules/image/js/plugins/drupalimagestyle/plugin.js new file mode 100644 index 0000000000..8d45a3681c --- /dev/null +++ b/core/modules/image/js/plugins/drupalimagestyle/plugin.js @@ -0,0 +1,114 @@ +/** + * @file + * Drupal Image Style plugin. + * + * This alters the existing CKEditor image2 widget plugin, which is already + * altered by the Drupal Image plugin, to allow for the data-image-style + * attribute to be set. + * + * @ignore + */ + +(function (CKEDITOR) { + + 'use strict'; + + CKEDITOR.plugins.add('drupalimagestyle', { + requires: 'drupalimage', + + beforeInit: function beforeInit(editor) { + // Override the image2 widget definition to handle the additional + // data-image-style attributes. + editor.on('widgetDefinition', function (event) { + var widgetDefinition = event.data; + if (widgetDefinition.name !== 'image') { + return; + } + // Override default features definitions for drupalimagestyle. + CKEDITOR.tools.extend(widgetDefinition.features, { + drupalimagestyle: { + requiredContent: 'img[data-image-style]' + } + }, true); + + // Override requiredContent & allowedContent. + var requiredContent = widgetDefinition.requiredContent.getDefinition(); + requiredContent.attributes['data-image-style'] = ''; + widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent); + widgetDefinition.allowedContent.img.attributes['!data-image-style'] = true; + + // Decorate downcast(). + var originalDowncast = widgetDefinition.downcast; + widgetDefinition.downcast = function (element) { + var img = originalDowncast.call(this, element); + if (!img) { + img = findElementByName(element, 'img'); + } + if (this.data.hasOwnProperty('data-image-style') && this.data['data-image-style'] !== '') { + img.attributes['data-image-style'] = this.data['data-image-style']; + } + return img; + }; + + // Decorate upcast(). + var originalUpcast = widgetDefinition.upcast; + widgetDefinition.upcast = function (element, data) { + if (element.name !== 'img' || !element.attributes['data-entity-type'] || !element.attributes['data-entity-uuid']) { + return; + } + // Don't initialize on pasted fake objects. + else if (element.attributes['data-cke-realelement']) { + return; + } + + // Parse the data-image-style attribute. + data['data-image-style'] = element.attributes['data-image-style']; + + // Upcast after parsing so correct element attributes are parsed. + element = originalUpcast.call(this, element, data); + + return element; + }; + + // Protected; keys of the widget data to be sent to the Drupal dialog. + // Append to the values defined by the drupalimage plugin. + // @see core/modules/ckeditor/js/plugins/drupalimage/plugin.js + CKEDITOR.tools.extend(widgetDefinition._mapDataToDialog, { + 'data-image-style': 'data-image-style' + }); + // Low priority to ensure drupalimage's event handler runs first. + }, null, null, 20); + } + }); + + /** + * Finds an element by its name. + * + * Function will check first the passed element itself and then all its + * children in DFS order. + * + * @param {CKEDITOR.htmlParser.element} element + * The element to search. + * @param {string} name + * The element name to search for. + * + * @return {?CKEDITOR.htmlParser.element} + * The found element, or null. + */ + function findElementByName(element, name) { + if (element.name === name) { + return element; + } + + var found = null; + element.forEach(function (el) { + if (el.name === name) { + found = el; + // Stop here. + return false; + } + }, CKEDITOR.NODE_ELEMENT); + return found; + } + +})(CKEDITOR); diff --git a/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php b/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php new file mode 100644 index 0000000000..ac92e8dfd6 --- /dev/null +++ b/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php @@ -0,0 +1,63 @@ +hasAssociatedFilterFormat()) { + return FALSE; + } + + // Automatically enable this plugin if the text format associated with this + // text editor uses the filter_image_style filter and the DrupalImage button + // is enabled. + $format = $editor->getFilterFormat(); + if ($format->filters('filter_image_style')->status) { + $toolbarButtons = CKEditorPluginManager::getEnabledButtons($editor); + return in_array('DrupalImage', $toolbarButtons); + } + + return FALSE; + } + +} diff --git a/core/modules/image/src/Plugin/Filter/FilterImageStyle.php b/core/modules/image/src/Plugin/Filter/FilterImageStyle.php new file mode 100644 index 0000000000..2a95084eec --- /dev/null +++ b/core/modules/image/src/Plugin/Filter/FilterImageStyle.php @@ -0,0 +1,323 @@ +entityTypeManager = $entity_type_manager; + $this->entityRepository = $entity_repository; + $this->imageFactory = $image_factory; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity.repository'), + $container->get('image.factory'), + $container->get('renderer') + ); + } + + /** + * {@inheritdoc} + */ + public function process($text, $langcode) { + if (stristr($text, 'data-image-style') !== FALSE) { + // Load all image styles so each image found in the text can be checked + // against a valid image style. + $image_styles = $this->getAllowedImageStyleIds(); + + $dom = Html::load($text); + $xpath = new \DOMXPath($dom); + + // Process each image element found with the necessary attributes. + /** @var \DOMElement $dom_element */ + foreach ($xpath->query('//*[@data-entity-type="file" and @data-entity-uuid and @data-image-style]') as $dom_element) { + // Get the UUID and image style for the file. + $file_uuid = $dom_element->getAttribute('data-entity-uuid'); + $image_style_id = $dom_element->getAttribute('data-image-style'); + + // If the image style is not a valid one, then don't transform the HTML. + if (empty($file_uuid) || !in_array($image_style_id, $image_styles)) { + continue; + } + if (!$this->entityRepository->loadEntityByUuid('file', $file_uuid)) { + continue; + } + + // Transform the HTML for the img element by applying an image style. + $altered_img_markup = $this->getImageStyleHtml($file_uuid, $image_style_id, $dom_element); + $altered_img = $dom->createDocumentFragment(); + $altered_img->appendXML($altered_img_markup); + $dom_element->parentNode->replaceChild($altered_img, $dom_element); + } + + return new FilterProcessResult(Html::serialize($dom)); + } + + // Process the filter if no image style img elements are found. + return new FilterProcessResult($text); + } + + /** + * Loads image styles. + * + * @param array $ids + * Optional array of image style IDs to load. If omitted, all image styles + * will be returned. + * + * @return \Drupal\image\ImageStyleInterface[] + * The image styles. + */ + protected function loadImageStyles(array $ids = NULL) { + return $this->entityTypeManager->getStorage('image_style')->loadMultiple($ids); + } + + /** + * Returns the machine names of the allowed image styles. + * + * @return string[] + * The machine names of the allowed image styles. + */ + protected function getAllowedImageStyleIds() { + if (!empty($this->settings['allowed_styles'])) { + return $this->settings['allowed_styles']; + } + // If no image styles are selected, then all are allowed. + return array_keys($this->loadImageStyles()); + } + + /** + * Returns the allowed image styles. + * + * @return \Drupal\image\ImageStyleInterface[] + * The allowed image styles. + */ + public function getAllowedImageStyles() { + $ids = !empty($this->settings['allowed_styles']) ? $this->settings['allowed_styles'] : NULL; + return $this->loadImageStyles($ids); + } + + /** + * Gets the the width and height of an image based on the file UUID. + * + * @param string $file_uuid + * The UUID for the file. + * + * @return array + * The image information. + */ + protected function getImageInfo($file_uuid) { + /** @var \Drupal\file\FileInterface $file */ + $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid); + + // Determine uri, width and height of the source image. + $image_uri = $image_width = $image_height = NULL; + $image = $this->imageFactory->get($file->getFileUri()); + if ($image->isValid()) { + $image_uri = $file->getFileUri(); + $image_width = $image->getWidth(); + $image_height = $image->getHeight(); + } + + return [ + 'uri' => $image_uri, + 'width' => $image_width, + 'height' => $image_height, + ]; + } + + /** + * Removes attributes that will be generated from image style theme function. + * + * @param \DOMElement $dom_element + * The DOM element for the img element. + * + * @return array + * The attributes array. + */ + protected function prepareImageAttributes(\DOMElement $dom_element) { + // Remove attributes that are generated by the image style. + $dom_element->removeAttribute('width'); + $dom_element->removeAttribute('height'); + $dom_element->removeAttribute('src'); + + // Make sure all non-regenerated attributes are retained. + $attributes = []; + for ($i = 0; $i < $dom_element->attributes->length; $i++) { + $attr = $dom_element->attributes->item($i); + $attributes[$attr->name] = $attr->value; + } + + return $attributes; + } + + /** + * Gets the HTML for the image element after image style is applied. + * + * @param string $file_uuid + * The UUID for the file. + * @param string $image_style_id + * The ID for the image style. + * @param \DOMElement $dom_element + * The DOM element for the image element. + * + * @return string + * The img element with the image style applied. + */ + protected function getImageStyleHtml($file_uuid, $image_style_id, \DOMElement $dom_element) { + $image_info = $this->getImageInfo($file_uuid); + + // Remove attributes that will be generated by the image style. + $attributes = $this->prepareImageAttributes($dom_element); + + // Re-render as an image style. + $image = [ + '#theme' => 'image_style', + '#style_name' => $image_style_id, + '#uri' => $image_info['uri'], + '#width' => $image_info['width'], + '#height' => $image_info['height'], + '#attributes' => $attributes, + ]; + + $output = $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$image) { + return $this->renderer->render($image); + }); + + return $output; + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $image_styles = $this->loadImageStyles(); + $options = array_map(function (ImageStyleInterface $image_style) { + return $image_style->label(); + }, $image_styles); + $form['allowed_styles'] = [ + '#type' => 'select', + '#title' => $this->t('Allowed image styles'), + '#options' => $options, + '#default_value' => $this->settings['allowed_styles'], + '#description' => $this->t('The image styles that can be used. If none are selected then all image styles can be used.'), + '#multiple' => TRUE, + // Limit the select box in length if there are a large number of image + // styles. + '#size' => min(20, count($image_styles)), + ]; + return $form; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + $dependencies = parent::calculateDependencies(); + + // Add the image styles that are allowed by this filter as dependencies. + foreach ($this->getAllowedImageStyles() as $image_style) { + $dependencies[$image_style->getConfigDependencyKey()][] = $image_style->getConfigDependencyName(); + } + return $dependencies; + } + + /** + * {@inheritdoc} + */ + public function tips($long = FALSE) { + if ($long) { + $image_styles = $this->getAllowedImageStyleIds(); + $image_styles = implode(', ', $image_styles); + $list = new TranslatableMarkup("$image_styles"); + return t('

You can display images using site-wide styles by adding a data-image-style attribute, whose values is one of the image style machine names: @image-style-machine-name-list.

', ['@image-style-machine-name-list' => $list]); + } + return t('You can display images using site-wide styles by adding a data-image-style attribute.'); + } + +} diff --git a/core/modules/image/tests/src/Functional/FilterImageStyleTest.php b/core/modules/image/tests/src/Functional/FilterImageStyleTest.php new file mode 100644 index 0000000000..40c99e1d44 --- /dev/null +++ b/core/modules/image/tests/src/Functional/FilterImageStyleTest.php @@ -0,0 +1,121 @@ +format = FilterFormat::create([ + 'format' => $this->randomMachineName(), + 'name' => $this->randomString(), + 'filters' => [ + 'filter_html' => [ + 'status' => TRUE, + 'settings' => [ + 'allowed_html' => '', + ], + ], + 'filter_image_style' => ['status' => TRUE], + 'editor_file_reference' => ['status' => TRUE], + ], + ]); + $this->format->save(); + + $user = $this->drupalCreateUser(['access content', 'administer nodes']); + $this->drupalLogin($user); + + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + } + + /** + * Helper function to create a test node with configurable image html. + */ + protected function nodeHelper($image_html) { + $node = $this->drupalCreateNode([ + 'type' => 'page', + 'title' => $this->randomString(), + 'body' => [ + [ + 'format' => $this->format->id(), + 'value' => $image_html, + ], + ], + ]); + $node->save(); + + $this->drupalGet($node->toUrl()); + $this->assertSession()->statusCodeEquals(200); + } + + /** + * Tests that images not uploaded through media module are unmolested. + */ + public function testImageNoStyle() { + $file_url = Url::fromUri('base:core/themes/bartik/screenshot.png')->toString(); + + $image_html = ''; + $this->nodeHelper($image_html); + + /** @var \Behat\Mink\Element\NodeElement $img_element */ + $image_element = $this->getSession()->getPage()->find('css', "img"); + $this->assertNotEmpty($image_element); + + $this->assertFalse($image_element->hasAttribute('class')); + $this->assertEquals($file_url, $image_element->getAttribute('src')); + $this->assertEquals('220', $image_element->getAttribute('width')); + $this->assertFalse($image_element->hasAttribute('height')); + } + + /** + * Tests image style modification of images. + */ + public function testImageStyle() { + $this->assertArrayHasKey('medium', $this->container->get('entity_type.manager')->getStorage('image_style')->loadMultiple()); + + $file = File::create(['uri' => 'core/themes/bartik/screenshot.png']); + $file->save(); + + $image_html = ''; + $this->nodeHelper($image_html); + + /** @var \Behat\Mink\Element\NodeElement $img_element */ + $image_element = $this->getSession()->getPage()->find('css', 'img.image-style-medium'); + $this->assertNotEmpty($image_element); + + $this->assertContains('medium', $image_element->getAttribute('src')); + $this->assertEquals('220', $image_element->getAttribute('width')); + $this->assertEquals('164', $image_element->getAttribute('height')); + } + +} diff --git a/core/modules/image/tests/src/Functional/Update/ImageUpdateTest.php b/core/modules/image/tests/src/Functional/Update/ImageUpdateTest.php index c3a5ddf270..865f898c43 100644 --- a/core/modules/image/tests/src/Functional/Update/ImageUpdateTest.php +++ b/core/modules/image/tests/src/Functional/Update/ImageUpdateTest.php @@ -32,11 +32,11 @@ public function testPostUpdateImageStylesDependencies() { // Check that view display 'node.article.default' doesn't depend on image // style 'image.style.large'. $dependencies = $this->config($view)->get('dependencies.config'); - $this->assertFalse(in_array('image.style.large', $dependencies)); + $this->assertNotContains('image.style.large', $dependencies); // Check that form display 'node.article.default' doesn't depend on image // style 'image.style.thumbnail'. $dependencies = $this->config($form)->get('dependencies.config'); - $this->assertFalse(in_array('image.style.thumbnail', $dependencies)); + $this->assertNotContains('image.style.thumbnail', $dependencies); // Run updates. $this->runUpdates(); @@ -44,11 +44,35 @@ public function testPostUpdateImageStylesDependencies() { // Check that view display 'node.article.default' depend on image style // 'image.style.large'. $dependencies = $this->config($view)->get('dependencies.config'); - $this->assertTrue(in_array('image.style.large', $dependencies)); + $this->assertContains('image.style.large', $dependencies); // Check that form display 'node.article.default' depend on image style // 'image.style.thumbnail'. $dependencies = $this->config($view)->get('dependencies.config'); - $this->assertTrue(in_array('image.style.large', $dependencies)); + $this->assertContains('image.style.large', $dependencies); + } + + /** + * Tests image_post_update_enable_filter_image_style(). + * + * @see image_post_update_enable_filter_image_style() + */ + public function testPostUpdateFilterImageStyle() { + // Check that the 'basic_html' and 'full_html' formats do not have the image + // style filter before starting the update. + $config_factory = \Drupal::configFactory(); + $basic_html_data = $config_factory->get('filter.format.basic_html'); + $this->assertNull($basic_html_data->get('filters.filter_image_style')); + $full_html_data = $config_factory->get('filter.format.full_html'); + $this->assertNull($full_html_data->get('filters.filter_image_style')); + + // Run updates. + $this->runUpdates(); + + // Check that the filter_format entities have been updated. + $basic_html_data = $config_factory->get('filter.format.basic_html'); + $this->assertNotNull($basic_html_data->get('filters.filter_image_style')); + $full_html_data = $config_factory->get('filter.format.full_html'); + $this->assertNotNull($full_html_data->get('filters.filter_image_style')); } } diff --git a/core/modules/image/tests/src/FunctionalJavascript/AddImageTest.php b/core/modules/image/tests/src/FunctionalJavascript/AddImageTest.php new file mode 100644 index 0000000000..4f65cf06d9 --- /dev/null +++ b/core/modules/image/tests/src/FunctionalJavascript/AddImageTest.php @@ -0,0 +1,102 @@ + 'filtered_html', + 'name' => $this->randomString(), + 'filters' => [ + 'filter_image_style' => ['status' => TRUE], + ], + ])->save(); + + $this->drupalCreateContentType(['type' => 'page', 'name' => 'Basic page']); + + Editor::create([ + 'format' => 'filtered_html', + 'editor' => 'ckeditor', + ])->save(); + + $user = $this->drupalCreateUser([ + 'access content', + 'administer nodes', + 'create page content', + 'use text format filtered_html', + ]); + $this->drupalLogin($user); + } + + /** + * Tests if an image can be placed inline with the data-image-style attribute. + */ + public function testDataImageStyleElement() { + $image_url = Url::fromUri('base:core/themes/bartik/screenshot.png')->toString(); + + $this->drupalGet('node/add/page'); + $this->assertSession()->statusCodeEquals(200); + + $page = $this->getSession()->getPage(); + // Wait for the ckeditor toolbar elements to appear (loading is done). + $image_button_selector = 'a.cke_button__drupalimage'; + $this->assertJsCondition("jQuery('$image_button_selector').length > 0"); + + $image_button = $page->find('css', $image_button_selector); + $this->assertNotEmpty($image_button); + $image_button->click(); + + $url_input = $this->assertSession()->waitForField('attributes[src]'); + $this->assertNotEmpty($url_input); + $url_input->setValue($image_url); + + $alt_input = $page->findField('attributes[alt]'); + $this->assertNotEmpty($alt_input); + $alt_input->setValue('asd'); + + $image_style_input_name = 'attributes[data-image-style]'; + $this->assertNotEmpty($page->findField($image_style_input_name)); + $page->selectFieldOption($image_style_input_name, 'thumbnail'); + + // To prevent 403s on save, we re-set our request (cookie) state. + $this->prepareRequest(); + + // @todo: Switch to using NodeElement::click() on the button or + // NodeElement::submit() on the form when #2831506 is fixed. + // @see https://www.drupal.org/node/2831506 + $script = "jQuery('input[id^=\"edit-actions-save-modal\"]').click()"; + $this->getSession()->executeScript($script); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $source_button = $page->find('css', 'a.cke_button__source'); + $this->assertNotEmpty($source_button); + $source_button->click(); + + $this->assertContains('data-image-style="thumbnail"', $page->find('css', 'textarea.cke_source')->getValue()); + } + +} diff --git a/core/modules/image/tests/src/Kernel/EditorImageStyleDialogTest.php b/core/modules/image/tests/src/Kernel/EditorImageStyleDialogTest.php new file mode 100644 index 0000000000..5c456afa1b --- /dev/null +++ b/core/modules/image/tests/src/Kernel/EditorImageStyleDialogTest.php @@ -0,0 +1,173 @@ +installEntitySchema('file'); + $this->installSchema('system', ['key_value_expire']); + $this->installSchema('node', ['node_access']); + $this->installSchema('file', ['file_usage']); + $this->installConfig(['node']); + + // Install the image module config so we have the medium image style. + $this->installConfig('image'); + + // Create a node type for testing. + $type = NodeType::create(['type' => 'page', 'name' => 'page']); + $type->save(); + node_add_body_field($type); + $this->installEntitySchema('user'); + $this->container->get('router.builder')->rebuild(); + } + + /** + * Fixture to consolidate tasks while making filter status configurable. + * + * @param bool $enable_image_filter + * Whether to activate filter_image_style in the text format. + * + * @return array|\Symfony\Component\HttpFoundation\Response + * The submitted form. + */ + protected function setUpForm($enable_image_filter) { + $format = FilterFormat::create([ + 'format' => $this->randomMachineName(), + 'name' => $this->randomString(), + 'weight' => 0, + 'filters' => [ + 'filter_image_style' => ['status' => $enable_image_filter], + ], + ]); + $format->save(); + + // Set up text editor. + /** @var \Drupal\editor\EditorInterface $editor */ + $editor = Editor::create([ + 'format' => $format->id(), + 'editor' => 'ckeditor', + 'image_upload' => [ + 'max_size' => 100, + 'scheme' => 'public', + 'directory' => '', + 'status' => TRUE, + ], + ]); + $editor->save(); + + /** @var \Drupal\file\FileInterface $file */ + $file = file_save_data(file_get_contents($this->root . '/core/modules/image/sample.png'), 'public://'); + + $input = [ + 'editor_object' => [ + 'src' => file_url_transform_relative($file->getFileUri()), + 'alt' => 'Balloons floating above a field.', + 'data-entity-type' => 'file', + 'data-entity-uuid' => $file->uuid(), + ], + 'dialogOptions' => [ + 'title' => 'Edit Image', + 'dialogClass' => 'editor-image-dialog', + 'autoResize' => 'true', + ], + '_drupal_ajax' => '1', + 'ajax_page_state' => [ + 'theme' => 'bartik', + 'theme_token' => 'some-token', + 'libraries' => '', + ], + ]; + if ($enable_image_filter) { + $input['editor_object']['data-image-style'] = 'medium'; + } + + $form_state = (new FormState()) + ->setRequestMethod('POST') + ->setUserInput($input) + ->addBuildInfo('args', [$editor]); + + /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */ + $form_builder = $this->container->get('form_builder'); + $form_object = new EditorImageDialog(\Drupal::entityTypeManager()->getStorage('file')); + $form_id = $form_builder->getFormId($form_object, $form_state); + $form = []; + + /** @var \Drupal\Core\Render\RendererInterface $renderer */ + $renderer = \Drupal::service('renderer'); + $renderer->executeInRenderContext(new RenderContext(), function () use (&$form, $form_builder, $form_id, $form_state) { + $form = $form_builder->retrieveForm($form_id, $form_state); + $form_builder->prepareForm($form_id, $form, $form_state); + $form_builder->processForm($form_id, $form, $form_state); + }); + + return $form; + } + + /** + * Tests that style selection is hidden when filter_image_style is disabled. + */ + public function testDialogNoStyles() { + $form = $this->setUpForm(FALSE); + + $this->assertFalse($form['image_style']['selection']['#access']); + $this->assertFalse($form['image_style']['link']['#access']); + } + + /** + * Tests EditorImageDialog when filter_image_style is enabled. + */ + public function testDialogStyles() { + $form = $this->setUpForm(TRUE); + + $this->assertEquals(['', 'large', 'medium', 'thumbnail'], array_keys($form['image_style']['selection']['#options'])); + + $this->assertEquals('medium', $form['image_style']['selection']['#default_value']); + } + +} diff --git a/core/modules/system/tests/src/Functional/Update/FilterHtmlUpdateTest.php b/core/modules/system/tests/src/Functional/Update/FilterHtmlUpdateTest.php index 09e40aa983..00b72dbca5 100644 --- a/core/modules/system/tests/src/Functional/Update/FilterHtmlUpdateTest.php +++ b/core/modules/system/tests/src/Functional/Update/FilterHtmlUpdateTest.php @@ -39,7 +39,7 @@ public function testAllowedHtmlUpdate() { // Make sure we have the expected values after the update. $filters_after = [ - 'basic_html' => '