diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc
index 0dff9f5..7e6bda7 100644
--- a/core/modules/ckeditor/ckeditor.admin.inc
+++ b/core/modules/ckeditor/ckeditor.admin.inc
@@ -57,7 +57,7 @@
       $value = $button['image_alternative' . $rtl];
     }
     elseif (isset($button['image'])) {
-      $value = theme('image', array('uri' => $button['image' . $rtl], 'title' => $button['label']));
+      $value = '<a href="#" class="cke_button" role="button" title="' . $button['label'] . '" aria-label="' . $button['label'] . '"><span class="cke_button_icon">' . theme('image', array('uri' => $button['image' . $rtl], 'title' => $button['label'])) . '</span></a>';
     }
     else {
       $value = '?';
diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module
index c53fefa..5676cc6 100644
--- a/core/modules/ckeditor/ckeditor.module
+++ b/core/modules/ckeditor/ckeditor.module
@@ -20,9 +20,12 @@
     'title' => 'Drupal behavior to enable CKEditor on textareas.',
     'version' => VERSION,
     'js' => array(
-      $module_path . '/js/ckeditor.js' => array(),
+      $module_path . '/js/ckeditor.js' => array('group' => JS_DEFAULT),
       array('data' => $settings, 'type' => 'setting'),
     ),
+    'css' => array(
+      $module_path . '/css/ckeditor.css' => array(),
+    ),
     'dependencies' => array(
       array('system', 'drupal'),
       array('ckeditor', 'ckeditor'),
diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css
index 272e3ea..41af514 100644
--- a/core/modules/ckeditor/css/ckeditor.admin.css
+++ b/core/modules/ckeditor/css/ckeditor.admin.css
@@ -85,8 +85,10 @@
   text-indent: -9999px;
   width: 16px;
 }
-ul.ckeditor-buttons li a:focus {
+ul.ckeditor-buttons li a:focus,
+ul.ckeditor-multiple-buttons li a:focus {
   z-index: 11; /* Ensure focused buttons show their outline on all sides. */
+  outline: 1px dotted #333;
 }
 ul.ckeditor-buttons li:first-child a {
   border-top-left-radius: 2px; /* LTR */
diff --git a/core/modules/ckeditor/css/ckeditor.css b/core/modules/ckeditor/css/ckeditor.css
new file mode 100644
index 0000000..08d5716
--- /dev/null
+++ b/core/modules/ckeditor/css/ckeditor.css
@@ -0,0 +1,19 @@
+.ckeditor-dialog-loading {
+  position: absolute;
+  top: 0;
+  width: 100%;
+  text-align: center;
+}
+
+.ckeditor-dialog-loading-link {
+  border-radius: 0 0 5px 5px;
+  border: 1px solid #B6B6B6;
+  border-top: none;
+  background: white;
+  padding: 3px 10px;
+  box-shadow: 0 0 10px -3px #000;
+  display: inline-block;
+  font-size: 14px;
+  position: relative;
+  top: 0;
+}
diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js
index f5d4317..19c6689 100644
--- a/core/modules/ckeditor/js/ckeditor.js
+++ b/core/modules/ckeditor/js/ckeditor.js
@@ -100,4 +100,83 @@
 
 };
 
+Drupal.ckeditor = {
+  /**
+   * Variable storing the current dialog's save callback.
+   */
+  saveCallack: null,
+
+  /**
+   * Open a dialog for a Drupal-based plugin.
+   *
+   * This dynamically loads jQuery UI (if necessary) using the Drupal AJAX
+   * framework, then opens a dialog at the specified Drupal path.
+   *
+   * @param editor
+   *   The CKEditor instance that is opening the dialog.
+   * @param options
+   *   An object of options that affect the behavior of the dialog. Possible
+   *   options include:
+   *   - callback: A function to be called upon saving the dialog.
+   */
+  openDialog: function (editor, url, existingValues, saveCallback, modalSettings) {
+    // Locate a suitable place to display our loading indicator.
+    var $target = $(editor.container.$);
+    if (editor.elementMode === CKEDITOR.ELEMENT_MODE_REPLACE) {
+      $target = $target.find('.cke_contents');
+    }
+
+    // Remove any previous loading indicator.
+    $target.css('position', 'relative').find('.ckeditor-dialog-loading').remove();
+
+    // Generate a pseudo-AJAX object for ajax.js to use in making this request.
+    Drupal.settings.ajax = Drupal.settings.ajax || {};
+    Drupal.settings.ajax['ckeditor-dialog'] = {
+      accepts: 'application/vnd.drupal-modal',
+      selector: '.ckeditor-dialog-loading-link a',
+      url: url,
+      event: 'click',
+      progress: { 'type': 'throbber' },
+      submit: {
+        editor_object: existingValues
+      }
+    };
+
+    // Add a new AJAX link, click it, then display it while loading.
+    var $content = $('<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link"><a href="' + url +'">' + Drupal.t('Loading...') + '</a></span></div>');
+    $content.appendTo($target);
+    Drupal.attachBehaviors($content[0]);
+    $content.find('a').trigger('click');
+
+    // After a short delay, show the loading throbber.
+    window.setTimeout(function() {
+      $content.find('span').animate({ top: '0px' });
+    }, 250);
+
+    // Store the save callback to be executed when this dialog is closed.
+    Drupal.ckeditor.saveCallback = saveCallback;
+  }
+};
+
+// Respond to new dialogs that are opened by CKEditor, closing the AJAX loader.
+$(window).on('dialog:beforecreate', function(e, dialog, $element, settings) {
+  $('.ckeditor-dialog-loading').animate({ top: '-40px' }, function() {
+    $(this).remove();
+  });
+});
+
+// Respond to dialogs that are saved, sending data back to CKEditor.
+$(window).on('editor:dialogsave', function(e, values) {
+  if (Drupal.ckeditor.saveCallback) {
+    Drupal.ckeditor.saveCallback(values);
+  }
+});
+
+// Respond to dialogs that are closed, removing the current save handler.
+$(window).on('dialog:afterclose', function(e, dialog, $element, values) {
+  if (Drupal.ckeditor.saveCallback) {
+    Drupal.ckeditor.saveCallback = null;
+  }
+});
+
 })(Drupal, CKEDITOR, jQuery);
diff --git a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
new file mode 100644
index 0000000..e8c0122
--- /dev/null
+++ b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
@@ -0,0 +1,126 @@
+/**
+ * @file
+ * Drupal Image plugin.
+ */
+
+(function ($, Drupal, drupalSettings, CKEDITOR) {
+
+"use strict";
+
+CKEDITOR.plugins.add('drupalimage', {
+  init: function (editor) {
+    var pluginName = 'drupalimage';
+
+    // Register the toolbar button.
+    editor.ui.addButton('DrupalImage', {
+      label: editor.lang.common.image,
+      command: 'image',
+      icon: drupalSettings.basePath + drupalSettings.ckeditor.modulePath + '/js/plugins/drupalimage/image.png'
+    });
+
+    // Register the image command.
+    editor.addCommand('image', {
+      allowedContent: 'img[alt,src,width,height,class,id,lang,longdesc,title]',
+      requiredContent: 'img[alt,src,width,height]',
+      exec: function (editor) {
+        var imageElement = getSelectedImage(editor);
+        var imageDOMElement = null;
+        var existingValues = {};
+        if (imageElement && imageElement.$) {
+          imageDOMElement = imageElement.$;
+
+          // Width and height are populated by actual dimensions.
+          existingValues.width = imageDOMElement ? imageDOMElement.width : '';
+          existingValues.height = imageDOMElement ? imageDOMElement.height : '';
+          // Populate all other attributes by their specified attribute values.
+          var attribute = null;
+          for (var key = 0; key < imageDOMElement.attributes.length; key++) {
+            attribute = imageDOMElement.attributes.item(key);
+            existingValues[attribute.nodeName.toLowerCase()] = attribute.nodeValue;
+          }
+        }
+
+        var saveCallback = function(returnValues) {
+          // Create a new image element if needed.
+          if (!imageElement && returnValues['attributes']['src']) {
+            imageElement = editor.document.createElement('img');
+            imageElement.setAttribute('alt', '');
+            editor.insertElement(imageElement);
+          }
+          // Delete the image if the src was removed.
+          if (imageElement && !returnValues['attributes']['src']) {
+            imageElement.remove();
+          }
+          // Update the image properties.
+          else {
+            for (var key in returnValues['attributes']) {
+              if (returnValues['attributes'].hasOwnProperty(key)) {
+                // Update the property if a value is specified.
+                if (returnValues['attributes'][key].length > 0) {
+                  imageElement.setAttribute(key, returnValues['attributes'][key]);
+                }
+                // Delete the property if set to an empty string.
+                else {
+                  imageElement.removeAttribute(key);
+                }
+              }
+            }
+          }
+        };
+        var modalSettings = {
+          dialogClass: 'editor-image-dialog',
+        };
+        Drupal.ckeditor.openDialog(editor, editor.config.drupalImage_dialogUrl, existingValues, saveCallback, modalSettings);
+      },
+      modes: { wysiwyg : 1 },
+      canUndo: true
+    });
+
+    // Double clicking an image opens its properties.
+    editor.on('doubleclick', function(event) {
+      var element = event.data.element;
+      if (element.is('img') && !element.data('cke-realelement') && !element.isReadOnly()) {
+        editor.getCommand('image').exec();
+      }
+    });
+
+    // If the "menu" plugin is loaded, register the menu items.
+    if (editor.addMenuItems) {
+      editor.addMenuItems({
+        image: {
+          label: editor.lang.image.menu,
+          command : 'image',
+          group: 'image'
+        }
+      });
+    }
+
+    // If the "contextmenu" plugin is loaded, register the listeners.
+    if (editor.contextMenu) {
+      editor.contextMenu.addListener(function (element, selection) {
+        if (getSelectedImage(editor, element)) {
+          return { image: CKEDITOR.TRISTATE_OFF };
+        }
+      });
+    }
+  }
+});
+
+/**
+ * Finds an img tag anywhere in the current editor selection.
+ */
+function getSelectedImage (editor, element) {
+  if (!element) {
+    var sel = editor.getSelection();
+    var selectedText = sel.getSelectedText().replace(/^\s\s*/, '').replace(/\s\s*$/, '');
+    var isElement = sel.getType() === CKEDITOR.SELECTION_ELEMENT;
+    var isEmptySelection = sel.getType() === CKEDITOR.SELECTION_TEXT && selectedText.length === 0;
+    element = (isElement || isEmptySelection) && sel.getSelectedElement();
+  }
+
+  if (element && element.is('img') && !element.data('cke-realelement') && !element.isReadOnly()) {
+    return element;
+  }
+}
+
+})(jQuery, Drupal, drupalSettings, CKEDITOR);
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php
index 4050be1..445d9c4 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php
@@ -39,4 +39,17 @@
     return FALSE;
   }
 
+  /**
+   * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getDependencies().
+   */
+  function getDependencies(Editor $editor) {
+    return array();
+  }
+
+  /**
+   * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getLibraries().
+   */
+  function getLibraries(Editor $editor) {
+    return array();
+  }
 }
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php
index 601cafb..58c83ac 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginInterface.php
@@ -41,6 +41,31 @@
   public function isInternal();
 
   /**
+   * Returns a list of plugins this plugin requires.
+   *
+   * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor
+   *   A configured text editor object.
+   * @return array
+   *   An unindexed array of plugin names this plugin requires. Each plugin is
+   *   is identified by its annotated ID.
+   */
+  public function getDependencies(Editor $editor);
+
+  /**
+   * Returns a list of libraries this plugin requires.
+   *
+   * These libraries will be attached to the text_format element on which the
+   * editor is being loaded.
+   *
+   * @param \Drupal\editor\Plugin\Core\Entity\Editor $editor
+   *   A configured text editor object.
+   * @return array
+   *   An array of libraries suitable for usage in a render API #attached
+   *   property.
+   */
+  public function getLibraries(Editor $editor);
+
+  /**
    * Returns the Drupal root-relative file path to the plugin JavaScript file.
    *
    * Note: this does not use a Drupal library because this uses CKEditor's API,
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php
index cbb75f6..ffcac7a 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php
@@ -65,6 +65,7 @@
     $plugins = array_keys($this->getDefinitions());
     $toolbar_buttons = array_unique(NestedArray::mergeDeepArray($editor->settings['toolbar']['buttons']));
     $enabled_plugins = array();
+    $additional_plugins = array();
 
     foreach ($plugins as $plugin_id) {
       $plugin = $this->createInstance($plugin_id);
@@ -74,19 +75,29 @@
       }
 
       $enabled = FALSE;
+      // Enable this plugin if it provides a button that has been enabled.
       if ($plugin instanceof CKEditorPluginButtonsInterface) {
         $plugin_buttons = array_keys($plugin->getButtons());
         $enabled = (count(array_intersect($toolbar_buttons, $plugin_buttons)) > 0);
       }
+      // Otherwise enable this plugin if it declares itself as enabled.
       if (!$enabled && $plugin instanceof CKEditorPluginContextualInterface) {
         $enabled = $plugin->isEnabled($editor);
       }
 
       if ($enabled) {
         $enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile();
+        // Check if this plugin has dependencies that also need to be enabled.
+        $additional_plugins = array_merge($additional_plugins, array_diff($plugin->getDependencies($editor), $additional_plugins));
       }
     }
 
+    // Add the list of dependent plugins.
+    foreach ($additional_plugins as $plugin_id) {
+      $plugin = $this->createInstance($plugin_id);
+      $enabled_plugins[$plugin_id] = ($plugin->isInternal()) ? NULL : $plugin->getFile();
+    }
+
     // Always return plugins in the same order.
     asort($enabled_plugins);
 
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php
new file mode 100644
index 0000000..82d0da3
--- /dev/null
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ckeditor\Plugin\ckeditor\plugin\DrupalImage.
+ */
+
+namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
+
+use Drupal\ckeditor\CKEditorPluginBase;
+use Drupal\ckeditor\Annotation\CKEditorPlugin;
+use Drupal\Core\Annotation\Translation;
+use Drupal\editor\Plugin\Core\Entity\Editor;
+
+/**
+ * Defines the "drupalimage" plugin.
+ *
+ * @CKEditorPlugin(
+ *   id = "drupalimage",
+ *   label = @Translation("Drupal image"),
+ *   module = "ckeditor"
+ * )
+ */
+class DrupalImage extends CKEditorPluginBase {
+
+  /**
+   * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getFile().
+   */
+  public function getFile() {
+    return drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimage/plugin.js';
+  }
+
+  /**
+   * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getLibraries().
+   */
+  public function getLibraries(Editor $editor) {
+    return array(
+      array('system', 'drupal.ajax'),
+    );
+  }
+
+  /**
+   * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getConfig().
+   */
+  public function getConfig(Editor $editor) {
+    return array(
+      'drupalImage_dialogUrl' => url('editor/dialog/image/' . $editor->format),
+    );
+  }
+
+  /**
+   * Implements \Drupal\ckeditor\Plugin\CKEditorPluginButtonsInterface::getButtons().
+   */
+  public function getButtons() {
+    return array(
+      'DrupalImage' => array(
+        'label' => t('Image'),
+        'image' => drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimage/image.png',
+      ),
+    );
+  }
+
+}
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php
index 4516762..1a4697c 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php
@@ -47,7 +47,6 @@
     $config = array(
       'customConfig' => '', // Don't load CKEditor's config.js file.
       'pasteFromWordPromptCleanup' => TRUE,
-      'removeDialogTabs' => 'image:Link;image:advanced;link:advanced',
       'resize_dir' => 'vertical',
       'keystrokes' =>  array(
         // 0x11000 is CKEDITOR.CTRL, see http://docs.ckeditor.com/#!/api/CKEDITOR-property-CTRL.
@@ -193,11 +192,6 @@
       'Format' => array(
         'label' => t('HTML block format'),
         'image_alternative' => '<a href="#" role="button" aria-label="' . t('Format') . '"><span class="ckeditor-button-dropdown">' . t('Format') . '<span class="ckeditor-button-arrow"></span></span></a>',
-      ),
-      // "image" plugin.
-      'Image' => array(
-        'label' => t('Image'),
-        'image_alternative' => $button('image'),
       ),
       // "table" plugin.
       'Table' => array(
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php
index 8cee06e..a0bd76e 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php
@@ -69,7 +69,7 @@
     // Case 1: no CKEditor plugins.
     $definitions = array_keys($this->manager->getDefinitions());
     sort($definitions);
-    $this->assertIdentical(array('internal', 'stylescombo'), $definitions, 'No CKEditor plugins found besides the built-in ones.');
+    $this->assertIdentical(array('drupalimage', 'internal', 'stylescombo'), $definitions, 'No CKEditor plugins found besides the built-in ones.');
     $this->assertIdentical(array(), $this->manager->getEnabledPlugins($editor), 'Only built-in plugins are enabled.');
     $this->assertIdentical(array('internal' => NULL), $this->manager->getEnabledPlugins($editor, TRUE), 'Only the "internal" plugin is enabled.');
 
@@ -82,7 +82,7 @@
     // Case 2: CKEditor plugins are available.
     $plugin_ids = array_keys($this->manager->getDefinitions());
     sort($plugin_ids);
-    $this->assertIdentical(array('internal', 'llama', 'llama_button', 'llama_contextual', 'llama_contextual_and_button', 'stylescombo'), $plugin_ids, 'Additional CKEditor plugins found.');
+    $this->assertIdentical(array('drupalimage', 'internal', 'llama', 'llama_button', 'llama_contextual', 'llama_contextual_and_button', 'stylescombo'), $plugin_ids, 'Additional CKEditor plugins found.');
     $this->assertIdentical(array(), $this->manager->getEnabledPlugins($editor), 'Only the internal plugins are enabled.');
     $this->assertIdentical(array('internal' => NULL), $this->manager->getEnabledPlugins($editor, TRUE), 'Only the "internal" plugin is enabled.');
 
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
index 846b93a..73abb70 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
@@ -79,6 +79,7 @@
       'toolbar' => $this->getDefaultToolbarConfig(),
       'contentsCss' => $this->getDefaultContentsCssConfig(),
       'extraPlugins' => '',
+      'removePlugins' => 'image',
       'language' => 'en',
       'stylesSet' => FALSE,
       'drupalExternalPlugins' => array(),
@@ -98,6 +99,7 @@
     $expected_config['toolbar'][] = '/';
     $expected_config['format_tags'] = 'p;h4;h5;h6';
     $expected_config['extraPlugins'] = 'llama_contextual,llama_contextual_and_button';
+    $expected_config['removePlugins'] = 'image';
     $expected_config['drupalExternalPlugins']['llama_contextual'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual.js');
     $expected_config['drupalExternalPlugins']['llama_contextual_and_button'] = file_create_url('core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.js');
     $expected_config['contentsCss'][] = file_create_url('core/modules/ckeditor/tests/modules/ckeditor_test.css');
@@ -224,7 +226,7 @@
     return array(
       'customConfig' => '',
       'pasteFromWordPromptCleanup' => TRUE,
-      'removeDialogTabs' => 'image:Link;image:advanced;link:advanced',
+      'removeDialogTabs' => 'link:advanced',
       'resize_dir' => 'vertical',
       'keystrokes' =>  array(array(0x110000 + 75, 'link'), array(0x110000 + 76, NULL)),
     );
diff --git a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/Llama.php b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/Llama.php
index 78e20e9..35a8db6 100644
--- a/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/Llama.php
+++ b/core/modules/ckeditor/tests/modules/lib/Drupal/ckeditor_test/Plugin/CKEditorPlugin/Llama.php
@@ -34,6 +34,20 @@
 class Llama extends PluginBase implements CKEditorPluginInterface {
 
   /**
+   * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getDependencies().
+   */
+  function getDependencies(Editor $editor) {
+    return array();
+  }
+
+  /**
+   * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getLibraries().
+   */
+  function getLibraries(Editor $editor) {
+    return array();
+  }
+
+  /**
    * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal().
    */
   function isInternal() {
@@ -48,7 +62,7 @@
   }
 
   /**
-   * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getButtons().
+   * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::getConfig().
    */
   public function getConfig(Editor $editor) {
     return array();
diff --git a/core/modules/editor/css/editor.dialog.css b/core/modules/editor/css/editor.dialog.css
new file mode 100644
index 0000000..4f71f1a
--- /dev/null
+++ b/core/modules/editor/css/editor.dialog.css
@@ -0,0 +1,8 @@
+/**
+ * @file
+ * Styles for text editors.
+ */
+.editor-image-dialog {
+  width: 80%;
+  max-width: 500px;
+}
diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
index 0f317b9..a439b15 100644
--- a/core/modules/editor/editor.module
+++ b/core/modules/editor/editor.module
@@ -79,6 +79,20 @@
     ),
   );
 
+  $libraries['drupal.editor.dialog'] = array(
+    'title' => 'Text Editor Dialog',
+    'version' => VERSION,
+    'js' => array(
+      $path . '/js/editor.dialog.js' => array(),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'drupal.dialog'),
+      array('system', 'drupal.ajax'),
+      array('system', 'drupalSettings'),
+    ),
+  );
+
   $libraries['edit.formattedTextEditor.editor'] = array(
     'title' => 'Formatted text editor',
     'version' => VERSION,
diff --git a/core/modules/editor/editor.routing.yml b/core/modules/editor/editor.routing.yml
index 0bb56cf..05cddcc 100644
--- a/core/modules/editor/editor.routing.yml
+++ b/core/modules/editor/editor.routing.yml
@@ -5,3 +5,9 @@
   requirements:
     _permission: 'access in-place editing'
     _access_edit_entity_field: 'TRUE'
+editor_image_dialog:
+  pattern: '/editor/dialog/image/{filter_format}'
+  defaults:
+    _form: '\Drupal\editor\Form\EditorImageDialog'
+  requirements:
+    _filter_access: 'TRUE'
diff --git a/core/modules/editor/js/editor.dialog.js b/core/modules/editor/js/editor.dialog.js
new file mode 100644
index 0000000..301d391
--- /dev/null
+++ b/core/modules/editor/js/editor.dialog.js
@@ -0,0 +1,20 @@
+/**
+ * @todo D8: remove this when http://drupal.org/node/1870764 lands
+ */
+
+(function ($, Drupal) {
+
+"use strict";
+
+/**
+ * Command to save the contents of an editor-provided modal.
+ *
+ * This command does not close the open modal. It should be followed by a call
+ * to Drupal.ajax.prototype.commands.closeDialog.
+ */
+Drupal.ajax.prototype.commands.editorDialogSave = function (ajax, response, status) {
+  console.log(response.values);
+  $(window).trigger('editor:dialogsave', [response.values]);
+};
+
+})(jQuery, Drupal);
diff --git a/core/modules/editor/lib/Drupal/editor/Ajax/EditorDialogSave.php b/core/modules/editor/lib/Drupal/editor/Ajax/EditorDialogSave.php
new file mode 100644
index 0000000..047336f
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/Ajax/EditorDialogSave.php
@@ -0,0 +1,46 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\editor\Ajax\EditorDialogSave.
+ */
+
+namespace Drupal\editor\Ajax;
+
+use Drupal\Core\Ajax\CommandInterface;
+
+/**
+ * Provides an AJAX command for saving the contents of an editor dialog.
+ *
+ * This command is implemented in editor.dialog.js in
+ * Drupal.ajax.prototype.commands.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;
+  }
+
+  /**
+   * Implements \Drupal\Core\Ajax\CommandInterface::render().
+   */
+  public function render() {
+    return array(
+      'command' => 'editorDialogSave',
+      'values' => $this->values,
+    );
+  }
+
+}
diff --git a/core/modules/editor/lib/Drupal/editor/EditorController.php b/core/modules/editor/lib/Drupal/editor/EditorController.php
index 2968454..c48b894 100644
--- a/core/modules/editor/lib/Drupal/editor/EditorController.php
+++ b/core/modules/editor/lib/Drupal/editor/EditorController.php
@@ -9,8 +9,11 @@
 
 use Symfony\Component\DependencyInjection\ContainerAware;
 use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\OpenModalDialogCommand;
+use Drupal\Core\Ajax\CloseModalDialogCommand;
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\editor\Ajax\GetUntransformedTextCommand;
+use Drupal\filter\Plugin\Core\Entity\FilterFormat;
 
 /**
  * Returns responses for Editor module routes.
@@ -44,4 +47,12 @@
     return $response;
   }
 
+  public function imageDialog(FilterFormat $filter_format = NULL) {
+    $response = new AjaxResponse();
+
+    $response->addCommand(new OpenModalDialogCommand(t('Insert image'), 'test'));
+
+    return $response;
+  }
+
 }
diff --git a/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php b/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php
new file mode 100644
index 0000000..3ff73ab
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\editor\Form\EditorImageDialog.
+ */
+
+namespace Drupal\editor\Form;
+
+use Drupal\Core\ControllerInterface;
+use Drupal\Core\Form\FormInterface;
+use Drupal\filter\Plugin\Core\Entity\FilterFormat;
+use Drupal\Core\Ajax\AjaxResponse;
+use Drupal\Core\Ajax\HtmlCommand;
+use Drupal\editor\Ajax\EditorDialogSave;
+use Drupal\Core\Ajax\CloseModalDialogCommand;
+
+/**
+ * Provides a configuration import form.
+ */
+class EditorImageDialog implements FormInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'editor_image_dialog';
+  }
+
+  /**
+   * {@inheritdoc}
+   *
+   * @param \Drupal\filter\Plugin\Core\Entity\FilterFormat $filter_format
+   *   The filter format for which this dialog corresponds.
+   */
+  public function buildForm(array $form, array &$form_state, FilterFormat $filter_format = NULL) {
+    // The default values are set directly from $_POST, provided by the
+    // editor plugin opening the dialog.
+    $input = isset($form_state['input']['editor_object']) ? $form_state['input']['editor_object'] : array();
+
+    $form['#tree'] = TRUE;
+    $form['#attached']['library'][] = array('editor', 'drupal.editor.dialog');
+    drupal_add_library('editor', 'drupal.editor.dialog');
+
+    // The #title attribute on the form will be set as the modal title.
+    $form['#title'] = isset($input['src']) ? t('Update image') : t('Insert image');
+
+    // Everything under the "attributes" key is merged directly into the
+    // generate img tag's attributes.
+    $form['attributes']['src'] = array(
+      '#title' => t('URL'),
+      '#type' => 'textfield',
+      '#default_value' => isset($input['src']) ? $input['src'] : '',
+      '#maxlength' => 2048,
+      '#required' => TRUE,
+    );
+
+    $form['attributes']['alt'] = array(
+      '#title' => t('Alternative text'),
+      '#type' => 'textfield',
+      '#default_value' => isset($input['alt']) ? $input['alt'] : '',
+      '#maxlength' => 2048,
+    );
+    $form['dimensions'] = array(
+      '#type' => 'item',
+      '#title' => t('Image size'),
+      '#field_prefix' => '<div class="container-inline">',
+      '#field_suffix' => '</div>',
+    );
+    $form['dimensions']['width'] = array(
+      '#title' => t('Width'),
+      '#title_display' => 'invisible',
+      '#type' => 'number',
+      '#default_value' => isset($input['width']) ? $input['width'] : '',
+      '#size' => 8,
+      '#maxlength' => 8,
+      '#min' => 1,
+      '#max' => 99999,
+      '#placeholder' => 'width',
+      '#field_suffix' => ' x ',
+      '#parents' => array('attributes', 'width'),
+    );
+    $form['dimensions']['height'] = array(
+      '#title' => t('Height'),
+      '#title_display' => 'invisible',
+      '#type' => 'number',
+      '#default_value' => isset($input['height']) ? $input['height'] : '',
+      '#size' => 8,
+      '#maxlength' => 8,
+      '#min' => 1,
+      '#max' => 99999,
+      '#placeholder' => 'height',
+      '#field_suffix' => 'pixels',
+      '#parents' => array('attributes', 'height'),
+    );
+
+    $form['actions'] = array(
+      '#type' => 'actions',
+    );
+    $form['actions']['save_modal'] = array(
+      '#type' => 'submit',
+      '#value' => t('Save'),
+      // No regular submit-handler. This form only works via JavaScript.
+      '#submit' => array(),
+      '#ajax' => array(
+        'callback' => array($this, 'submitForm'),
+      ),
+    );
+
+    return $form;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, array &$form_state) {
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, array &$form_state) {
+    $response = new AjaxResponse();
+
+    if (form_get_errors()) {
+      $output = drupal_render($form);
+      $output = '<div>' . theme('status_messages') . $output . '</div>';
+      $response->addCommand(new HtmlCommand('#drupal-modal', $output));
+    }
+    else {
+      $response->addCommand(new EditorDialogSave($form_state['values']));
+      $response->addCommand(new CloseModalDialogCommand());
+    }
+
+    return $response;
+  }
+
+}
