diff --git a/core/modules/image/image.module b/core/modules/image/image.module index 07f9fc3..12fa4fd 100644 --- a/core/modules/image/image.module +++ b/core/modules/image/image.module @@ -486,10 +486,10 @@ function image_field_config_delete(FieldConfigInterface $field) { } /** - * Implements hook_form_FORM_ID_alter() for EditorImageDialog. + * Implements hook_form_FORM_ID_alter(). * * Alters the image dialog form for text editor, to allow the user to select an - * image style. + * image style. * * @see \Drupal\editor\Form\EditorImageDialog::buildForm() */ @@ -512,6 +512,7 @@ function image_form_editor_image_dialog_alter(&$form, FormStateInterface $form_s '#default_value' => isset($image_element['data-image-style']) ? $image_element['data-image-style'] : NULL, '#options' => $image_style_options, '#required' => TRUE, + '#parents' => array('attributes', 'data-image-style'), ); $form['actions']['save_modal']['#validate'][] = 'image_form_editor_image_dialog_validate'; @@ -538,17 +539,13 @@ function image_form_editor_image_dialog_validate(array &$form, FormStateInterfac /** @var \Drupal\file\FileInterface $file */ $file = File::load($form_state->getValue('fid')[0]); - // Get the URI of the image from the file. $uri = $file->getFileUri(); - // Transform absolute image URLs to relative image URLs: prevent problems - // on multisite set-ups and prevent mixed content errors. - $image_url = file_create_url($uri); - $image_url = file_url_transform_relative($image_url); // 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. - $attributes['src'] = $image_style->buildUrl($image_url); + $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); @@ -557,7 +554,7 @@ function image_form_editor_image_dialog_validate(array &$form, FormStateInterfac // Get the original width and height of the image. $dimensions = array( 'width' => $image->getWidth(), - 'height' => $image->getHeight() + 'height' => $image->getHeight(), ); // Transform the 'width' and 'height' dimensions of the image based on the diff --git a/core/modules/image/js/plugins/drupalimagestyle/plugin.js b/core/modules/image/js/plugins/drupalimagestyle/plugin.js index bc7b7fa..ef8eae1 100644 --- a/core/modules/image/js/plugins/drupalimagestyle/plugin.js +++ b/core/modules/image/js/plugins/drupalimagestyle/plugin.js @@ -35,7 +35,7 @@ var requiredContent = widgetDefinition.requiredContent.getDefinition(); requiredContent.attributes['data-image-style'] = ''; widgetDefinition.requiredContent = new CKEDITOR.style(requiredContent); - widgetDefinition.allowedContent.img.attributes += ',!data-image-style'; + widgetDefinition.allowedContent.img.attributes['!data-image-style'] = true; // Decorate downcast(). var originalDowncast = widgetDefinition.downcast; diff --git a/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php b/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php index 6689e30..d076508 100644 --- a/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php +++ b/core/modules/image/src/Plugin/CKEditorPlugin/DrupalImageStyle.php @@ -63,12 +63,12 @@ public function isEnabled(Editor $editor) { } // Automatically enable this plugin if the text format associated with this - // text editor uses the filter_image_style filter and the drupalimagestyle + // 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('drupalimagestyle', $toolbarButtons); + 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 index bb29180..328daed 100644 --- a/core/modules/image/src/Plugin/Filter/FilterImageStyle.php +++ b/core/modules/image/src/Plugin/Filter/FilterImageStyle.php @@ -93,16 +93,14 @@ public static function create(ContainerInterface $container, array $configuratio */ public function process($text, $langcode) { if (stristr($text, 'data-image-style') !== FALSE) { - // Load all the image styles so each imgage found in the text can be - // checked to ensure it has a valid image style. + // Load all image styles so each image found in the text can be + // checked against a valid image style. $image_styles = $this->loadImageStyles(); - // Load the text that is being processed into XML to find images. $dom = HTML::load($text); $xpath = new \DOMXPath($dom); - // Process each img element DOM element found with the necessary - // attributes. + // Process each img 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. @@ -116,23 +114,12 @@ public function process($text, $langcode) { } // Transform the HTML for the img element by applying an image style. - $altered_img = $this->getImageStyleHtml($file_uuid, $image_style_id, $dom_element); - - // Load the altered HTML into a new DOMDocument and retrieve the element. - $updated_node = HTML::load($altered_img)->getElementsByTagName('body') - ->item(0) - ->childNodes - ->item(0); - - // Import the updated node from the new DOMDocument into the original - // one, importing also the child nodes of the updated node. - $updated_node = $dom->importNode($updated_node, TRUE); - - // Finally, replace the original image node with the new image node. - $dom_element->parentNode->replaceChild($updated_node, $dom_element); + $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); } - // Process the filter with the newly updated DOM. return new FilterProcessResult(HTML::serialize($dom)); } @@ -141,9 +128,10 @@ public function process($text, $langcode) { } /** - * Loads the image styles. + * Loads the available image style names. * * @return string[] + * Array keys of the available image styles. */ protected function loadImageStyles() { return array_keys($this->entityTypeManager->getStorage('image_style')->loadMultiple()); @@ -159,7 +147,7 @@ protected function loadImageStyles() { * The image information. */ protected function getImageInfo($file_uuid) { - /** @var \Drupal\file\FileInterface $file; */ + /** @var \Drupal\file\FileInterface $file */ $file = $this->entityRepository->loadEntityByUuid('file', $file_uuid); // Determine uri, width and height of the source image. @@ -174,7 +162,7 @@ protected function getImageInfo($file_uuid) { return [ 'uri' => $image_uri, 'width' => $image_width, - 'height' => $image_height + 'height' => $image_height, ]; } @@ -185,7 +173,7 @@ protected function getImageInfo($file_uuid) { * The DOM element for the img element. * * @return array - * The attributes array. + * The attributes array. */ protected function prepareImageAttributes(\DOMElement $dom_element) { // Remove attributes that are no longer needed. @@ -217,7 +205,7 @@ protected function prepareImageAttributes(\DOMElement $dom_element) { * @param string $image_style_id * The ID for the image style. * @param \DOMElement $dom_element - * The DOM element for the image element. + * The DOM element for the image element. * * @return string * The img element with the image style applied. @@ -238,9 +226,11 @@ protected function getImageStyleHtml($file_uuid, $image_style_id, \DOMElement $d '#attributes' => $attributes, ]; - $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$image) { + $output = $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$image) { return $this->renderer->render($image); }); + + return $output; } /** @@ -256,5 +246,7 @@ public function tips($long = FALSE) { else { 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 0000000..63528cb --- /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' => 1, + 'settings' => [ + 'allowed_html' => '', + ], + ], + 'filter_image_style' => ['status' => 1], + 'editor_file_reference' => ['status' => 1], + ], + ]); + $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->assertFalse(empty($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->assertTrue(array_key_exists('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->assertFalse(empty($image_element)); + + $this->assertFalse($image_element->hasAttribute('data-entity-type')); + $this->assertFalse($image_element->hasAttribute('data-entity-uuid')); + $this->assertFalse($image_element->hasAttribute('data-image-style')); + + $this->assertTrue(strpos($image_element->getAttribute('src'), 'medium')); + $this->assertEquals('220', $image_element->getAttribute('width')); + $this->assertEquals('164', $image_element->getAttribute('height')); + } + +} 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 0000000..2b33a5b --- /dev/null +++ b/core/modules/image/tests/src/FunctionalJavascript/AddImageTest.php @@ -0,0 +1,106 @@ + 'filtered_html', + 'name' => $this->randomString(), + 'filters' => [ + 'filter_image_style' => ['status' => 1], + ], + ]); + $format->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", 20000); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $image_button = $page->find('css', $image_button_selector); + $this->assertNotEmpty($image_button); + $image_button->click(); + // Wait for the modal form elements to appear (loading is done). + $this->assertJsCondition("jQuery('input[data-drupal-selector=\"edit-attributes-alt\"]').length > 0", 20000); + + $url_input = $page->findField('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(); + + // Using NodeElement::click() on the button or NodeElement::submit() on the + // form generated phantomjs-internal exceptions. This was the only recourse. + $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->assertSession()->assertWaitOnAjaxRequest(); + + $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 index a062273..9282c32 100644 --- a/core/modules/image/tests/src/Kernel/EditorImageStyleDialogTest.php +++ b/core/modules/image/tests/src/Kernel/EditorImageStyleDialogTest.php @@ -39,7 +39,7 @@ class EditorImageStyleDialogTest extends EntityKernelTestBase { 'image', 'node', 'system', - 'user' + 'user', ]; /** @@ -54,19 +54,40 @@ protected function setUp() { $this->installSchema('file', ['file_usage']); $this->installConfig(['node']); - // Add text formats. + // 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' => TRUE] + 'filter_image_style' => ['status' => $enable_image_filter], ], ]); $format->save(); // Set up text editor. - $this->editor = Editor::create([ + /** @var \Drupal\editor\EditorInterface $editor */ + $editor = Editor::create([ 'format' => $format->id(), 'editor' => 'ckeditor', 'image_upload' => [ @@ -76,23 +97,8 @@ protected function setUp() { 'status' => TRUE, ], ]); - $this->editor->save(); - - // 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(); - } + $editor->save(); - /** - * Tests that editor image dialog works as expected. - */ - public function testEditorImageStyleDialog() { /** @var \Drupal\file\FileInterface $file */ $file = file_save_data(file_get_contents($this->root . '/core/modules/image/sample.png'), 'public://'); @@ -102,7 +108,6 @@ public function testEditorImageStyleDialog() { 'alt' => 'Balloons floating above a field.', 'data-entity-type' => 'file', 'data-entity-uuid' => $file->uuid(), - 'data-image-style' => 'medium', ], 'dialogOptions' => [ 'title' => 'Edit Image', @@ -116,10 +121,14 @@ public function testEditorImageStyleDialog() { 'libraries' => '', ], ]; + if ($enable_image_filter) { + $input['editor_object']['data-image-style'] = 'medium'; + } + $form_state = (new FormState()) ->setRequestMethod('POST') ->setUserInput($input) - ->addBuildInfo('args', [$this->editor]); + ->addBuildInfo('args', [$editor]); /** @var \Drupal\Core\Form\FormBuilderInterface $form_builder */ $form_builder = $this->container->get('form_builder'); @@ -129,14 +138,34 @@ public function testEditorImageStyleDialog() { /** @var \Drupal\Core\Render\RendererInterface $renderer */ $renderer = \Drupal::service('renderer'); - $renderer->executeInRenderContext(new RenderContext(), function() use (&$form, $form_builder, $form_id, $form_state) { + $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); }); - // Image style should be medium. + return $form; + } + + /** + * Tests that style selection is hidden when filter_image_style is disabled. + */ + public function testDialogNoStyles() { + $form = $this->setUpForm(FALSE); + + $this->assertFalse(array_key_exists('image_style', $form)); + } + + /** + * 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']); + $this->assertEquals(TRUE, $form['image_style']['selection']['#required']); } } diff --git a/core/modules/image/tests/src/Unit/FilterImageStyleTest.php b/core/modules/image/tests/src/Unit/FilterImageStyleTest.php deleted file mode 100644 index ad1b047..0000000 --- a/core/modules/image/tests/src/Unit/FilterImageStyleTest.php +++ /dev/null @@ -1,106 +0,0 @@ - 'image']; - $entityTypeManager = $this->prophesize(EntityTypeManager::class); - $entityRepository = $this->prophesize(EntityRepository::class); - $imageFactory = $this->prophesize(ImageFactory::class); - $renderer = $this->prophesize(Renderer::class); - - $this->filterImageStyle = $this->getMockBuilder('Drupal\image\Plugin\Filter\FilterImageStyle') - ->setConstructorArgs([ - $configuration, - $plugin_id, - $plugin_definition, - $entityTypeManager->reveal(), - $entityRepository->reveal(), - $imageFactory->reveal(), - $renderer->reveal() - ]) - ->setMethods([ - 'loadImageStyles', - 'getImageStyleHtml' - ]) - ->getMock(); - } - - public function testProcessWithoutImage() { - $output = $this->filterImageStyle->process('', 'en'); - $this->assertEquals('', $output); - } - - /** - * @covers ::process - */ - public function testProcessWithImage() { - $original_src = 'image.png'; - $original_uuid = 'abcd-1234-ghij-5678'; - $original_image_style = 'medium'; - $original_width = '400'; - $original_height = '300'; - $original_alt = 'A wooly mammoth trumpets as a crevasse breaks open in the glacier.'; - - $original_img = '' . $original_alt .''; - $original_text = '

' . $original_img . '

'; - - $expected_src = 'styles/medium/public/inline-images/image.png'; - $expected_width = '200'; - $expected_height = '150'; - - $expected_img = '' . $original_alt .''; - $expected_text = '

' . $expected_img . '

'; - - $this->filterImageStyle - ->method('loadImageStyles') - ->willReturn([ - 'thumbnail', - 'medium', - 'large' - ]); - - $this->filterImageStyle - ->method('getImageStyleHtml') - ->with( - $this->equalTo($original_uuid), - $this->equalTo($original_image_style), - $this->anything() - ) - ->willReturn($expected_img); - - $output = $this->filterImageStyle->process($original_text, 'en'); - $this->assertEquals($expected_text, $output); - } -}