 core/misc/dialog.ajax.js                           |   50 ++++-
 core/misc/dialog.js                                |   37 ++++
 core/misc/dialog.theme.css                         |  103 ++++++++++
 core/misc/loading-small.gif                        |   25 +++
 core/misc/loading.gif                              |   43 ++++
 core/modules/ckeditor/ckeditor.admin.inc           |    2 +-
 core/modules/ckeditor/ckeditor.module              |    3 +
 core/modules/ckeditor/css/ckeditor-iframe.css      |    7 +
 core/modules/ckeditor/css/ckeditor.admin.css       |    4 +-
 core/modules/ckeditor/css/ckeditor.css             |   25 +++
 core/modules/ckeditor/js/ckeditor.js               |   97 +++++++++
 .../ckeditor/js/plugins/drupalimage/image.png      |    7 +
 .../ckeditor/js/plugins/drupalimage/plugin.js      |  136 +++++++++++++
 .../ckeditor/js/plugins/drupallink/link.png        |    3 +
 .../ckeditor/js/plugins/drupallink/plugin.js       |  207 ++++++++++++++++++++
 .../ckeditor/js/plugins/drupallink/unlink.png      |    7 +
 .../lib/Drupal/ckeditor/CKEditorPluginBase.php     |   15 +-
 .../Drupal/ckeditor/CKEditorPluginInterface.php    |   25 +++
 .../lib/Drupal/ckeditor/CKEditorPluginManager.php  |   11 ++
 .../ckeditor/Plugin/CKEditorPlugin/DrupalImage.php |   64 ++++++
 .../ckeditor/Plugin/CKEditorPlugin/DrupalLink.php  |   69 +++++++
 .../ckeditor/Plugin/CKEditorPlugin/Internal.php    |   25 ---
 .../lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php |   31 ++-
 .../Drupal/ckeditor/Tests/CKEditorAdminTest.php    |    4 +-
 .../Drupal/ckeditor/Tests/CKEditorLoadingTest.php  |    3 +-
 .../ckeditor/Tests/CKEditorPluginManagerTest.php   |   22 ++-
 .../lib/Drupal/ckeditor/Tests/CKEditorTest.php     |   23 ++-
 .../ckeditor_test/Plugin/CKEditorPlugin/Llama.php  |   16 +-
 core/modules/editor/css/editor.css                 |   15 ++
 core/modules/editor/editor.module                  |   17 ++
 core/modules/editor/editor.routing.yml             |   12 ++
 core/modules/editor/js/editor.dialog.js            |   21 ++
 .../lib/Drupal/editor/Ajax/EditorDialogSave.php    |   47 +++++
 .../editor/lib/Drupal/editor/EditorController.php  |    5 +
 .../lib/Drupal/editor/Form/EditorImageDialog.php   |  136 +++++++++++++
 .../lib/Drupal/editor/Form/EditorLinkDialog.php    |  103 ++++++++++
 .../lib/Drupal/editor/Plugin/EditorManager.php     |    1 +
 .../lib/Drupal/editor/Tests/EditorLoadingTest.php  |    2 +
 .../lib/Drupal/editor/Tests/EditorManagerTest.php  |    1 +
 core/modules/system/system.module                  |    4 +
 .../standard/config/editor.editor.basic_html.yml   |    6 +-
 .../standard/config/editor.editor.full_html.yml    |    6 +-
 core/themes/bartik/css/style.css                   |    2 +-
 core/themes/seven/jquery.ui.theme.css              |   49 -----
 44 files changed, 1376 insertions(+), 115 deletions(-)

diff --git a/core/misc/dialog.ajax.js b/core/misc/dialog.ajax.js
index 8b1f80f..bae03bf 100644
--- a/core/misc/dialog.ajax.js
+++ b/core/misc/dialog.ajax.js
@@ -8,13 +8,55 @@
   "use strict";
 
   Drupal.behaviors.dialog = {
-    attach: function () {
+    attach: function (context, settings) {
       // Provide a known 'drupal-modal' DOM element for Drupal-based modal
       // dialogs. Non-modal dialogs are responsible for creating their own
       // elements, since there can be multiple non-modal dialogs at a time.
       if (!$('#drupal-modal').length) {
         $('<div id="drupal-modal" />').hide().appendTo('body');
       }
+
+      // If a new form is being attached inside a dialog, remove and replace
+      // the dialog buttons with those from the new form.
+      var $dialog = $(context).closest('.ui-dialog-content');
+      if ($dialog && $dialog.dialog('option', 'drupalAutoButtons')) {
+        var buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog);
+        $dialog.dialog('option', 'buttons', buttons);
+      }
+    },
+    detach: function (context, settings) {
+      $(context).find('form').off('submit.dialogSubmit');
+    },
+
+    /**
+     * Scan a dialog for any primary buttons and move them to the button area.
+     *
+     * @param $dialog
+     *   An jQuery object containing the element that is the dialog target.
+     * @return
+     *   An array of buttons that need to be added to the button area.
+     */
+    prepareDialogButtons: function ($dialog) {
+      var buttons = [];
+      var $buttons = $dialog.find('.form-actions input[type=submit]');
+      $buttons.each(function () {
+        var $originalButton = $(this).hide();
+        buttons.push({
+          'text': $originalButton.html() || $originalButton.attr('value'),
+          'class': $originalButton.attr('class'),
+          'click': function (e) {
+            $originalButton.trigger('mousedown');
+            e.preventDefault();
+          }
+        });
+      });
+      if ($buttons.length) {
+        $dialog.find('form').on('submit.dialogSubmit', function (e) {
+          $buttons.first().trigger('mousedown');
+          e.preventDefault();
+        });
+      }
+      return buttons;
     }
   };
 
@@ -40,6 +82,12 @@
     response.method = 'html';
     ajax.commands.insert(ajax, response, status);
 
+    // Move the buttons to the jQuery UI dialog buttons area.
+    if (!response.dialogOptions.buttons) {
+      response.dialogOptions.drupalAutoButtons = true;
+      response.dialogOptions.buttons = Drupal.behaviors.dialog.prepareDialogButtons($dialog);
+    }
+
     // Open the dialog itself.
     response.dialogOptions = response.dialogOptions || {};
     var dialog = Drupal.dialog($dialog, response.dialogOptions);
diff --git a/core/misc/dialog.js b/core/misc/dialog.js
index 62cbeb1..44fd736 100644
--- a/core/misc/dialog.js
+++ b/core/misc/dialog.js
@@ -10,6 +10,7 @@
 
 drupalSettings.dialog = {
   autoOpen: true,
+  autoResize: true,
   dialogClass: '',
   close: function (e) {
     Drupal.detachBehaviors(e.target, null, 'unload');
@@ -23,6 +24,10 @@ Drupal.dialog = function (element, options) {
     // Trigger a global event to allow scripts to bind events to the dialog.
     $(window).trigger('dialog:beforecreate', [dialog, $element, settings]);
     $element.dialog(settings);
+    if (settings.autoResize !== 'false' && settings.autoResize !== false) {
+      $(window).on('resize.dialogResize scroll.dialogResize', autoResize);
+      resetPosition();
+    }
     dialog.open = true;
     $(window).trigger('dialog:aftercreate', [dialog, $element, settings]);
   }
@@ -32,11 +37,43 @@ Drupal.dialog = function (element, options) {
     $element.dialog('close');
     dialog.returnValue = value;
     dialog.open = false;
+    $(window).off('.dialogResize');
     $(window).trigger('dialog:afterclose', [dialog, $element]);
   }
 
+  /**
+   * Resets the current options for positioning.
+   *
+   * This is used as a window resize and scroll callback to reposition the
+   * jQuery UI dialog. Although not a built-in jQuery UI option, this can
+   * be disabled by setting autoResize: false in the options array when creating
+   * a new Drupal.dialog().
+   */
+  function resetPosition () {
+    var positionOptions = ['width', 'height', 'minWidth', 'minHeight', 'maxHeight', 'maxWidth', 'position'];
+    var windowHeight = $(window).height();
+    var adjustedOptions = $.extend({ position: { my: "center", at: "center", of: window }}, options);
+    var optionValue, adjustedValue;
+    for (var n = 0; n < positionOptions.length; n++) {
+      if (adjustedOptions[positionOptions[n]]) {
+        optionValue = adjustedOptions[positionOptions[n]];
+        // jQuery UI does not support percentages on heights, convert to pixels.
+        if (positionOptions[n].match(/height/i) && typeof optionValue === 'string' && optionValue.match(/%$/)) {
+          adjustedValue = parseInt(0.01 * parseInt(optionValue, 10) * windowHeight, 10);
+          // Don't force the dialog to be bigger vertically than needed.
+          if (positionOptions[n] === 'height' && $element.parent().outerHeight() < adjustedValue) {
+            adjustedValue = 'auto';
+          }
+          adjustedOptions[positionOptions[n]] = adjustedValue;
+        }
+      }
+    }
+    $element.dialog('option', adjustedOptions);
+  }
+
   var undef;
   var $element = $(element);
+  var autoResize = Drupal.debounce(resetPosition, 50);
   var dialog = {
     open: false,
     returnValue: undef,
diff --git a/core/misc/dialog.theme.css b/core/misc/dialog.theme.css
new file mode 100644
index 0000000..360e6ae
--- /dev/null
+++ b/core/misc/dialog.theme.css
@@ -0,0 +1,103 @@
+/**
+ * Presentational styles for Drupal dialogs.
+ */
+
+.ui-dialog {
+  position: absolute;
+  z-index: 1260;
+  overflow: visible;
+  color: #000;
+  background: #fff;
+  border: solid 1px #ccc;
+  padding: 0;
+}
+.ui-dialog .ui-dialog-titlebar {
+  font-weight: bold;
+  background: #f3f4ee;
+  border-style: solid;
+  border-radius: 0;
+  border-width: 0 0 1px 0;
+  border-color: #ccc;
+}
+.ui-dialog .ui-dialog-titlebar-close {
+  border: 0;
+  background: none;
+}
+.ui-dialog .ui-dialog-buttonpane {
+  margin-top: 0;
+  background: #f3f4ee;
+  padding: .3em 1em;
+  border-width: 1px 0 0 0;
+  border-color: #ccc;
+}
+.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset {
+  margin: 0;
+  padding: 0;
+}
+.ui-dialog .ui-dialog-buttonpane .ui-button-text-only .ui-button-text {
+  padding: 0;
+}
+
+.ui-dialog .ui-dialog-buttonpane button {
+  background: #fefefe;
+  background-image: -webkit-linear-gradient(top, #fefefe, #e0e0e0);
+  background-image: -moz-linear-gradient(top, #fefefe, #e0e0e0);
+  background-image: -o-linear-gradient(top, #fefefe, #e0e0e0);
+  background-image: linear-gradient(to bottom, #fefefe, #e0e0e0);
+  border: 1px solid #c8c8c8;
+  border-radius: 3px;
+  text-decoration: none;
+  padding: 6px 17px 6px 17px;
+}
+.ui-dialog .ui-dialog-buttonpane button:hover,
+.ui-dialog .ui-dialog-buttonpane button:focus {
+  background: #fefefe;
+  background-image: -webkit-linear-gradient(top, #fefefe, #eaeaea);
+  background-image: -moz-linear-gradient(top, #fefefe, #eaeaea);
+  background-image: -o-linear-gradient(top, #fefefe, #eaeaea);
+  background-image: linear-gradient(to bottom, #fefefe, #eaeaea);
+  -webkit-box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
+  box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
+  color: #2e2e2e;
+  text-decoration: none;
+}
+.ui-dialog .ui-dialog-buttonpane button:active {
+  border: 1px solid #c8c8c8;
+  background: #fefefe;
+  background-image: -webkit-linear-gradient(top, #eaeaea, #fefefe);
+  background-image: -moz-linear-gradient(top, #eaeaea, #fefefe);
+  background-image: -o-linear-gradient(top, #eaeaea, #fefefe);
+  background-image: linear-gradient(to bottom, #eaeaea, #fefefe);
+  -webkit-box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
+  box-shadow: 1px 1px 3px rgba(50, 50, 50, 0.1);
+  color: #2e2e2e;
+  text-decoration: none;
+  text-shadow: none;
+}
+
+/* Form action buttons are moved in dialogs. Remove empty space. */
+.ui-dialog .ui-dialog-content .form-actions {
+  padding: 0;
+  margin: 0;
+}
+.ui-dialog .ajax-progress-throbber {
+  /* Can't do center:50% middle: 50%, so approximate it for a typical window size. */
+  left: 49%;
+  position: fixed;
+  top: 48.5%;
+  z-index: 1000;
+  background-color: #232323;
+  background-image: url("loading-small.gif");
+  background-position: center center;
+  background-repeat: no-repeat;
+  border-radius: 7px;
+  height: 24px;
+  opacity: 0.9;
+  padding: 4px;
+  width: 24px;
+}
+.ui-dialog .ajax-progress-throbber .throbber,
+.ui-dialog .ajax-progress-throbber .message {
+  display: none;
+}
+
diff --git a/core/misc/loading-small.gif b/core/misc/loading-small.gif
new file mode 100644
index 0000000..5cbf6e7
--- /dev/null
+++ b/core/misc/loading-small.gif
@@ -0,0 +1,25 @@
+GIF89a    Ž{{{ssskkkcccZZZRRRJJJBBB:::111)))!!!   !NETSCAPE2.0   !
+  ,       &6MeK,kG8@qh/MGH:%#Gc4D2a8@@p2HfzH,%x"
+WrIix%pqX1*=##+="I#3g~"	">w	kX(_S(As}RTx (oBB4xZ	%q !
+  ,       'GQe04k<\nU6d	'U!`D % XeU(2H	`%)2(*PEš	b%K`1K:$2$P{({"#)1%%%	
+#k
+{"	?&*Evl [8w+!  !
+  ,       Ԡ'WUea<k,VDQJG7G`P6YQ4 ZCb4AQa^iU M!QCX#V
+
+n2  Q
+91 +1q~=wx:{e+"%"])<H*9H~1x%"t  !
+  ,       'gYe@kah<Z%VDeHDf!jd)X,"ϓ1
+Fj`:fQEdI:{*+:k$u"91>>	":)E*	)% # 
+"cI	(E+{`."W !
+  ,       ۠'w]eҲH뺉*S%Z"<bc"z$G`86F*>Q0DGѨU,5q@0EsJ:}aLG9}+#}%W1*:"R)L")D  n˺9R; ȶ#Ϊ!  !
+  ,      ߠ'$86pQx%N
+y<Rl"	BQHddzb`*!MLj옵F={2~5^|t"95""*~$	*	2 5	d#
+ "d	+
+oI5Uc pK  !
+  ,       'aeUk,bPަuG큫L.D"&D4a0ȒE2.ȍh0&b҅+v9ȕQI,JfEsHR2$bv#>#  {
+
+ 2
+H#"#$"0*	*+%{\"	%*NUȷLQ !	
+  ,       'ee4Ykb&ܴ8x,lh&8DRQQ4g5A)q_&dTBP@UF2A1*;$F$sm1"J$,+:+%	y%*	+
+	m#
+%"	"U^åA!  ;
\ No newline at end of file
diff --git a/core/misc/loading.gif b/core/misc/loading.gif
new file mode 100644
index 0000000..2dbcd62
--- /dev/null
+++ b/core/misc/loading.gif
@@ -0,0 +1,43 @@
+GIF89a0 0   ###'''(((---111555:::===CCCGGGKKKMMMQQQUUUZZZ^^^aaaggghhhmmmqqqwwwxxx{{{///333444>>>AAAEEEIIIOOORRRXXX```eeekkklllssszzz~~~&&&***,,,222666<<<JJJYYYrrrvvv|||...;;;HHHLLLPPPWWW\\\dddiii}}}%%%@@@DDDttt$$$+++777999]]]nnnuuuyyyFFFSSS___bbbppp)))                                                                                                                                                                                                                                                                  !NETSCAPE2.0   !-Made by Krasimira Nejcheva (www.loadinfo.net) ! 
+  ,    0 0  @pH,BApl: j vKf嚛X8kEz)Ï4wFD%}`%y	yP#/0)D3DG#0,C+}+B
+FD*k*D	ѽZƯ'CZؚ'NyҾ	Z0!M
+ѦNf< (Vra 5pEA(B@xxl;` ZD3̚HrQ÷p8HBH8"E"`P.PP0DMW@S Y/T @ p@@V^pPUDܸPp!maM-k!L㎜jp
+n 0HXfe+DO
+B B
+k!=1MUUs v=x y,T&L(!z6p` 7QoؿKw	q@,PX|&|X`t9Qy@ JIU 9E%@D | TO!  U %8MpP&и*<N@4@!Q4	B @Rs|c5!A6q ~A@_vЀu5	@h@A{F*K ! 
+  ,    0 0    56 JOOJCQ00TC;SQ<GGLT#4EAKTI8FJA57++$=CDARVVP94I,+MNϢSN	j@ŝ;'$=bhbJ4So W,AHb;46!a jtiE	' B@* 	Vp2@=(PrʍN=ܫT*G6R*%L &K@&)@ tQ	W߿) 
+4HHx;pcD'Mx1x>~@  1;d2 ;D@[ #/nҠ˪uW ٚhxHr·îݫWF?&9yk氅=x8<D%"I]6 '@$b@lPd@'9 ĉ<!!:T0ċC(Iyb0k@6x'=cID(n a:uJ ?(	c, FX4dIPv4HBlGA <YU@K(qi4TxV0YAhC6vmD=Չ ; D{G|z7Xث~J냆<kxl!5(Ew ;ѫ>Kj 7р.  ! 
+  ,    0 0   W WZZ<c##d<k,,+,k]'+,']W+We,eY`a[,d\cZci'jjJ;,_Wk0/#8ŎYcj'g8["СD4cwGh M|.ʡD: B&0NY:-0`xPP7t!|et3 ܳF̼GWt'Q3
+kmq4B"*PPƌL@8D.bbɒ"nW DWǅ 6ň2N5 1\
+n`hg$Oޢ56y̸ɂض
+1gxw\P[l"Fyg͟Cw#n徛_8^'`9m5Hٌ.Zc	yR~ p!e-&d-Ha,[< ? !Zt 9&	ab$
+r'(V;; A'2J."l '^&W4?$cl )X\L9ɕJfIbpa(]xC/8P$XL@)c^ Yx;,D8j	gAfJ@^z)^f8l$WJ(o  ;\c:_EEHЀE ! 
+  ,    0 0   HA W޼rÇcƌ8!j(
+?N$!#Ǔl҉?P,XAdKM*yOH-I;Ah!xEЛ	!O7'1&dA:$ 
+QB..q	O	UVrUrFIl'@k3RGŋ5`!N
+trf@y4]W47Lز%HF:Asf@w4jӸ>qP:z-	8Yܧ$+Cɩ4T(|ѓ`&,pۿOBQGw̡ZL!IWġ{`XA $TPTf1@M("wb}.!c$Rqr1x?DEI2ȤMōR" >b%_@2RD:n@r8! &6 
+9ys w(GXhLoa`7wr4LXQ@YqG
+)Gw*XL뮱A4jiȱ:;$ B-}hGr@`ȡJ;Cr]QGA,v0&ANzǭ pCWGms̑@A1u y(G_Jt2j3iuT,U;d u\AZAqlytD ! 
+  ,    0 0   HA h8ȰÆB!B3j$@ 5HD̀LtKQ9J /s`sOr2``JIS塂[|\8`F@6B!L@B̘0qdJ7ϖT	QRάlbUҥkv<Mx`F#YLw!B`XǂâY"͜7t !O}@1g 3":ڄ4.lQH6s7c< j<#	Y@rK#f<xe I&-{@_PQyt\!7 RD"+0X[T8>,  x+ Xh!-10`G=.B\B J% "F%b",~#(00Mb@th&
+#E*ڛ `i|$(L>EC!4$gp"^@iՑ~F é0p\F4PQF>)W>
+*]wƭ&[ڌG	ĢHF
+ji
+ў:FFQliĀ
+3;
+jH.{+
+.BU*:>4kA>ȩ,FlQ!7H#Hp7\@qCr'C<6@!ǐYʐG@B-҃A=P@ ! 
+  ,    0 0   H`AG =z䨡#JttNJwn ȱA9ƈcIǓ#)R$0:`RƘoDsaRdЀwzIIw<B9@ŋw^dul<<:Z	E<@+KJGF,xrqmJ`Bu!iNKz1CIr`#b3ёW=xZuI\윔A9sGRc#7$ΔtBtHt$ ͷtKd^8qrD"AWЃ820"&r2 Ʉ$B ~T!&+>'$(z ,v%5P#P Q.
+IG")>"^6NɖgؐfV0 <vE"	X@:	H	 $wh0F9*
+ c$S"t&wh# D&8x0X!U1UT
++t&xtZO: E'6|5Gj+6,kaBQxA0mtv@.5& drnEt4	VdB `&+IGp	'W'IB!O;EaxԀ&~pxBDWD9gS$XR$8<l"E0FH2QD\Azb'\=0o^II{ ! 
+  ,    0 0   HA `AxÇ4 A3D0j!>i@ǏG ɲ(崜	 ̏4H Orӣ5
+IhF,rD!ʁ9=p"?e Q7E9-CX{$8uX`ZրB5CjӪs r؋vB4VNv`Ea4Pڴ.$%܅G!40Ivc/~EѲAr{0֘]j H!/S5;|	5(s	FT?ܐ!Ƃn J`H|X`@fPHHX!ǒ%@" TS+2H}H c4jE8Ǝ0
+IFx(%(cif!b%2`_F !!YxQAHN	ȣxK]r	g!hPFal(+).h hAL`	2*F7.@B&C")/	Y:F{ls
+B@X0lIP	+0D&!D5a
+	T+n"|r@c~C
+
+4JB<e0j
+TnT 'BqzUh"GBbs{]02'hC=,0f5"$BfRö4 ! 
+  ,    0 0   HA ::ȰÆZ4h@Ë!Bdp:F2˂8>~L˛ v(sÅ#L)M
+h*  1XgH@NG_E#aY0Z^r5+)b&5ՆܺDp;ɷ1=
+Hp(aƍ>4k';YSFuCG^\MpU$nGk	jJT.i< (!K8<2@}Ztoy;>Cs&dD#C Â0p9R l0F4%R0`	Z8F  *xA@^x8.28~.fcJ<Ï@䅉DH=$I4y"W<СS(`he	zMe^y7za'Pm|^|F8eI$`g$ŢJ*f@ I'PQ(A@@&CpiBŨ	)pb"J	zĨxqi'g$e"&";Ey ,j `4\F-\@"A8 ĽM q@nj	'MpypoQ9t A4̰M8~A*!>4y1o񗸓DC927% C8M Bh@C`dH:"S@ ;
\ No newline at end of file
diff --git a/core/modules/ckeditor/ckeditor.admin.inc b/core/modules/ckeditor/ckeditor.admin.inc
index cd043ac..607c6af 100644
--- a/core/modules/ckeditor/ckeditor.admin.inc
+++ b/core/modules/ckeditor/ckeditor.admin.inc
@@ -63,7 +63,7 @@ function theme_ckeditor_settings_toolbar($variables) {
         '#uri' => $button['image' . $rtl],
         '#title' => $button['label'],
       );
-      $value = drupal_render($image);
+      $value = '<a href="#" class="cke_button" role="button" title="' . $button['label'] . '" aria-label="' . $button['label'] . '"><span class="cke_button_icon">' . drupal_render($image) . '</span></a>';
     }
     else {
       $value = '?';
diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module
index 84c713a..6a1b633 100644
--- a/core/modules/ckeditor/ckeditor.module
+++ b/core/modules/ckeditor/ckeditor.module
@@ -23,6 +23,9 @@ function ckeditor_library_info() {
       $module_path . '/js/ckeditor.js' => array(),
       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-iframe.css b/core/modules/ckeditor/css/ckeditor-iframe.css
index 54f4b3f..994d1d5 100644
--- a/core/modules/ckeditor/css/ckeditor-iframe.css
+++ b/core/modules/ckeditor/css/ckeditor-iframe.css
@@ -9,6 +9,13 @@ body {
   margin: 8px;
 }
 
+@media screen and (max-width: 600px) {
+  /* A font-size of 16px prevents iOS from zooming. */
+  body {
+    font-size: 16px;
+  }
+}
+
 ol, ul, dl {
   /* IE7: reset rtl list margin. (CKEditor issue #7334) */
   *margin-right: 0px;
diff --git a/core/modules/ckeditor/css/ckeditor.admin.css b/core/modules/ckeditor/css/ckeditor.admin.css
index 786e02a..1061f09 100644
--- a/core/modules/ckeditor/css/ckeditor.admin.css
+++ b/core/modules/ckeditor/css/ckeditor.admin.css
@@ -85,8 +85,10 @@ ul.ckeditor-buttons li .cke-icon-only {
   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..f45ec26
--- /dev/null
+++ b/core/modules/ckeditor/css/ckeditor.css
@@ -0,0 +1,25 @@
+.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;
+  -webkit-touch-callout: none;
+  -webkit-user-select: none;
+  -khtml-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+}
diff --git a/core/modules/ckeditor/js/ckeditor.js b/core/modules/ckeditor/js/ckeditor.js
index 18c4632..3c950f2 100644
--- a/core/modules/ckeditor/js/ckeditor.js
+++ b/core/modules/ckeditor/js/ckeditor.js
@@ -7,6 +7,10 @@ Drupal.editors.ckeditor = {
   attach: function (element, format) {
     this._loadExternalPlugins(format);
     this._ACF_HACK_to_support_blacklisted_attributes(element, format);
+    // Also pass settings that are Drupal-specific.
+    format.editorSettings.drupal = {
+      format: format.format
+    };
     return !!CKEDITOR.replace(element, format.editorSettings);
   },
 
@@ -175,6 +179,99 @@ Drupal.editors.ckeditor = {
       }
     });
   }
+
+};
+
+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 string url
+   *   The URL that contains the contents of the dialog.
+   * @param Object existingValues
+   *   Existing values that will be sent via POST to the url for the dialog
+   *   contents.
+   * @param Function saveCallback
+   *   A function to be called upon saving the dialog.
+   * @param Object dialogSettings
+   *   An object containing settings to be passed to the jQuery UI.
+   */
+  openDialog: function (editor, url, existingValues, saveCallback, dialogSettings) {
+    // 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();
+
+    // Add a consistent dialog class.
+    var classes = dialogSettings.dialogClass ? dialogSettings.dialogClass.split(' ') : [];
+    classes.push('editor-dialog');
+    dialogSettings.dialogClass = classes.join(' ');
+    dialogSettings.maxHeight = '95%';
+    dialogSettings.resizable = false;
+    dialogSettings.autoResize = $(window).width() > 600;
+
+    // Add a "Loading…" message, hide it underneath the CKEditor toolbar, create
+    // a Drupal.ajax instance to load the dialog and trigger it.
+    var $content = $('<div class="ckeditor-dialog-loading"><span style="top: -40px;" class="ckeditor-dialog-loading-link"><a>' + Drupal.t('Loading...') + '</a></span></div>');
+    $content.appendTo($target);
+    new Drupal.ajax('ckeditor-dialog', $content.find('a').get(0), {
+      accepts: 'application/vnd.drupal-modal',
+      dialog: dialogSettings,
+      selector: '.ckeditor-dialog-loading-link',
+      url: url,
+      event: 'ckeditor-internal.ckeditor',
+      progress: { 'type': 'throbber' },
+      submit: {
+        editor_object: existingValues
+      }
+    });
+    $content.find('a')
+      .on('click', function () { return false; })
+      .trigger('ckeditor-internal.ckeditor');
+
+    // After a short delay, show "Loading…" message.
+    window.setTimeout(function () {
+      $content.find('span').animate({ top: '0px' });
+    }, 1000);
+
+    // 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) {
+  if (Drupal.ckeditor.saveCallback) {
+    Drupal.ckeditor.saveCallback = null;
+  }
+});
+
 })(Drupal, CKEDITOR, jQuery);
diff --git a/core/modules/ckeditor/js/plugins/drupalimage/image.png b/core/modules/ckeditor/js/plugins/drupalimage/image.png
new file mode 100644
index 0000000..83cd553
--- /dev/null
+++ b/core/modules/ckeditor/js/plugins/drupalimage/image.png
@@ -0,0 +1,7 @@
+PNG
+
+   IHDR         (-S   PLTE                                          ZZZ```
+
+
+      			&&&   """###%%%)))***---666888999;;;<<<>>>@@@AAABBBDDDGGGIIIJJJKKKLLLNNNOOOPPPQQQRRRUUUYYYw    tRNS 
+!*LPU`tv~=K   IDATW` W*2KVG]yO HjZ e3 bOxt@+&0RՊX{]Ofbpٻyt<og۸u۶WC@C׾=t )7OSw:z    IENDB`
\ No newline at end of file
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..36f5492
--- /dev/null
+++ b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
@@ -0,0 +1,136 @@
+/**
+ * @file
+ * Drupal Image plugin.
+ */
+
+(function ($, Drupal, drupalSettings, CKEDITOR) {
+
+"use strict";
+
+CKEDITOR.plugins.add('drupalimage', {
+  init: function (editor) {
+    // Register the image command.
+    editor.addCommand('image', {
+      allowedContent: 'img[alt,src,width,height]',
+      requiredContent: 'img[alt,src,width,height]',
+      modes: { wysiwyg : 1 },
+      canUndo: true,
+      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;
+          }
+        }
+
+        function saveCallback (returnValues) {
+          // Save snapshot for undo support.
+          editor.fire('saveSnapshot');
+
+          // 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);
+                }
+              }
+            }
+          }
+        }
+
+        // Drupal.t() will not work inside CKEditor plugins because CKEditor
+        // loads the JavaScript file instead of Drupal. Pull translated strings
+        // from the plugin settings that are translated server-side.
+        var dialogSettings = {
+          title: imageDOMElement ? editor.config.drupalImage_dialogTitleEdit : editor.config.drupalImage_dialogTitleAdd,
+          dialogClass: 'editor-image-dialog'
+        };
+
+        // Open the dialog for the edit form.
+        Drupal.ckeditor.openDialog(editor, Drupal.url('editor/dialog/image/' + editor.config.drupal.format), existingValues, saveCallback, dialogSettings);
+      }
+    });
+
+    // Register the toolbar button.
+    if (editor.ui.addButton) {
+      editor.ui.addButton('DrupalImage', {
+        label: editor.lang.common.image,
+        command: 'image',
+        icon: this.path.replace(/plugin\.js.*/, 'image.png')
+      });
+    }
+
+    // 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/js/plugins/drupallink/link.png b/core/modules/ckeditor/js/plugins/drupallink/link.png
new file mode 100644
index 0000000..54e506a
--- /dev/null
+++ b/core/modules/ckeditor/js/plugins/drupallink/link.png
@@ -0,0 +1,3 @@
+PNG
+
+   IHDR         (-S   lPLTE                  XXX]]]PPPPPP                  #tRNS 68abcdg   hIDATxڭG0 {ZdD kWi ILi2uԱ*]x y2lބ{m6Ǫ:`ᘦR	2$:WYz    IENDB`
\ No newline at end of file
diff --git a/core/modules/ckeditor/js/plugins/drupallink/plugin.js b/core/modules/ckeditor/js/plugins/drupallink/plugin.js
new file mode 100644
index 0000000..5f591e7
--- /dev/null
+++ b/core/modules/ckeditor/js/plugins/drupallink/plugin.js
@@ -0,0 +1,207 @@
+/**
+ * @file
+ * Drupal Link plugin.
+ */
+
+(function ($, Drupal, drupalSettings, CKEDITOR) {
+
+"use strict";
+
+CKEDITOR.plugins.add('drupallink', {
+  init: function (editor) {
+    // Add the commands for link and unlink.
+    editor.addCommand('link', {
+      allowedContent: 'a[!href,target]',
+      requiredContent: 'a[href]',
+      modes: { wysiwyg : 1 },
+      canUndo: true,
+      exec: function (editor) {
+        var linkElement = getSelectedLink(editor);
+        var linkDOMElement = null;
+
+        // Set existing values based on selected element.
+        var existingValues = {};
+        if (linkElement && linkElement.$) {
+          linkDOMElement = linkElement.$;
+
+          // Populate an array with the link's current attributes.
+          var attribute = null;
+          for (var key = 0; key < linkDOMElement.attributes.length; key++) {
+            attribute = linkDOMElement.attributes.item(key);
+            existingValues[attribute.nodeName.toLowerCase()] = attribute.nodeValue;
+          }
+        }
+
+        // Prepare a save callback to be used upon saving the dialog.
+        var saveCallback = function (returnValues) {
+          // Save snapshot for undo support.
+          editor.fire('saveSnapshot');
+
+          // Create a new link element if needed.
+          if (!linkElement && returnValues.attributes.href) {
+            var selection = editor.getSelection();
+            var range = selection.getRanges(1)[0];
+
+            // Use link URL as text with a collapsed cursor.
+            if (range.collapsed) {
+              // Shorten mailto URLs to just the e-mail address.
+              var text = new CKEDITOR.dom.text(returnValues.attributes.href.replace(/^mailto:/, ''), editor.document);
+              range.insertNode(text);
+              range.selectNodeContents(text);
+            }
+
+            // Create the new link by applying a style to the new text.
+            var style = new CKEDITOR.style({ element: 'a', attributes: returnValues.attributes });
+            style.type = CKEDITOR.STYLE_INLINE;
+            style.applyToRange(range);
+            range.select();
+
+            // Set the link so individual properties may be set below.
+            linkElement = getSelectedLink(editor);
+          }
+          // Update the link properties.
+          else if (linkElement) {
+            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) {
+                  linkElement.setAttribute(key, returnValues.attributes[key]);
+                }
+                // Delete the property if set to an empty string.
+                else {
+                  linkElement.removeAttribute(key);
+                }
+              }
+            }
+          }
+        };
+        // Drupal.t() will not work inside CKEditor plugins because CKEditor
+        // loads the JavaScript file instead of Drupal. Pull translated strings
+        // from the plugin settings that are translated server-side.
+        var dialogSettings = {
+          title: linkElement ? editor.config.drupalLink_dialogTitleEdit : editor.config.drupalLink_dialogTitleAdd,
+          dialogClass: 'editor-link-dialog'
+        };
+
+        // Open the dialog for the edit form.
+        Drupal.ckeditor.openDialog(editor, Drupal.url('editor/dialog/link/' + editor.config.drupal.format), existingValues, saveCallback, dialogSettings);
+      }
+    });
+    editor.addCommand('unlink', {
+      contextSensitive: 1,
+      startDisabled: 1,
+      requiredContent: 'a[href]',
+      exec: function (editor) {
+        var style = new CKEDITOR.style({ element:'a', type: CKEDITOR.STYLE_INLINE, alwaysRemoveElement: 1 });
+        editor.removeStyle(style);
+      },
+      refresh: function ( editor, path ) {
+        var element = path.lastElement && path.lastElement.getAscendant('a', true);
+        if (element && element.getName() === 'a' && element.getAttribute('href') && element.getChildCount()) {
+          this.setState(CKEDITOR.TRISTATE_OFF);
+        }
+        else {
+          this.setState(CKEDITOR.TRISTATE_DISABLED);
+        }
+      }
+    });
+
+    editor.setKeystroke(CKEDITOR.CTRL + 75 /*K*/, 'link');
+
+    // Add buttons for link and unlink.
+    if (editor.ui.addButton) {
+      editor.ui.addButton('DrupalLink', {
+        label: editor.lang.link.toolbar,
+        command: 'link',
+        icon: this.path.replace(/plugin\.js.*/, 'link.png')
+      });
+      editor.ui.addButton('DrupalUnlink', {
+        label: editor.lang.link.unlink,
+        command: 'unlink',
+        icon: this.path.replace(/plugin\.js.*/, 'unlink.png')
+      });
+    }
+
+    editor.on('doubleclick', function (evt) {
+      var element = getSelectedLink(editor) || evt.data.element;
+
+      if (!element.isReadOnly()) {
+        if (element.is('a')) {
+          editor.getSelection().selectElement(element);
+          editor.getCommand('link').exec();
+        }
+      }
+    });
+
+    // If the "menu" plugin is loaded, register the menu items.
+    if (editor.addMenuItems) {
+      editor.addMenuItems({
+        link: {
+          label: editor.lang.link.menu,
+          command: 'link',
+          group: 'link',
+          order: 1
+        },
+
+        unlink: {
+          label: editor.lang.link.unlink,
+          command: 'unlink',
+          group: 'link',
+          order: 5
+        }
+      });
+    }
+
+    // If the "contextmenu" plugin is loaded, register the listeners.
+    if (editor.contextMenu) {
+      editor.contextMenu.addListener(function (element, selection) {
+        if (!element || element.isReadOnly()) {
+          return null;
+        }
+        var anchor = getSelectedLink(editor);
+        if (!anchor) {
+          return null;
+        }
+
+        var menu = {};
+        if (anchor.getAttribute('href') && anchor.getChildCount()) {
+          menu = { link: CKEDITOR.TRISTATE_OFF, unlink: CKEDITOR.TRISTATE_OFF };
+        }
+        return menu;
+      });
+    }
+  }
+});
+
+
+/**
+ * Get the surrounding link element of current selection.
+ *
+ * The following selection will all return the link element.
+ *
+ *  <a href="#">li^nk</a>
+ *  <a href="#">[link]</a>
+ *  text[<a href="#">link]</a>
+ *  <a href="#">li[nk</a>]
+ *  [<b><a href="#">li]nk</a></b>]
+ *  [<a href="#"><b>li]nk</b></a>
+ *
+ * @param {CKEDITOR.editor} editor
+ */
+function getSelectedLink(editor) {
+  var selection = editor.getSelection();
+  var selectedElement = selection.getSelectedElement();
+  if (selectedElement && selectedElement.is('a')) {
+    return selectedElement;
+  }
+
+  var range = selection.getRanges(true)[0];
+
+  if (range) {
+    range.shrink(CKEDITOR.SHRINK_TEXT);
+    return editor.elementPath(range.getCommonAncestor()).contains('a', 1);
+  }
+  return null;
+}
+
+})(jQuery, Drupal, drupalSettings, CKEDITOR);
diff --git a/core/modules/ckeditor/js/plugins/drupallink/unlink.png b/core/modules/ckeditor/js/plugins/drupallink/unlink.png
new file mode 100644
index 0000000..64056ad
--- /dev/null
+++ b/core/modules/ckeditor/js/plugins/drupallink/unlink.png
@@ -0,0 +1,7 @@
+PNG
+
+   IHDR         7   IDAT(c`!40l`NEY!
+/ /韫N@!贈ïgw!
+0Iw/c`9s{`2<f	!	M͏chfx,B#D硶ggE
+ ma6e/_;
+0,C37FW-%t˺ ASL2    IENDB`
\ No newline at end of file
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php
index 4050be1..34ab0bd 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginBase.php
@@ -33,10 +33,23 @@
 abstract class CKEditorPluginBase extends PluginBase implements CKEditorPluginInterface, CKEditorPluginButtonsInterface {
 
   /**
-   * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal().
+   * {@inheritdoc}
    */
   function isInternal() {
     return FALSE;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  function getDependencies(Editor $editor) {
+    return array();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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 acc33fe..3b1ad15 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/CKEditorPluginManager.php
@@ -65,6 +65,7 @@ public function getEnabledPlugins(Editor $editor, $include_internal_plugins = FA
     $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 @@ public function getEnabledPlugins(Editor $editor, $include_internal_plugins = FA
       }
 
       $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..84c31b2
--- /dev/null
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php
@@ -0,0 +1,64 @@
+<?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 {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFile() {
+    return drupal_get_path('module', 'ckeditor') . '/js/plugins/drupalimage/plugin.js';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLibraries(Editor $editor) {
+    return array(
+      array('system', 'drupal.ajax'),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfig(Editor $editor) {
+    return array(
+      'drupalImage_dialogTitleAdd' => t('Insert Image'),
+      'drupalImage_dialogTitleEdit' => t('Edit Image'),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  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/DrupalLink.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalLink.php
new file mode 100644
index 0000000..a60314c
--- /dev/null
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalLink.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\ckeditor\Plugin\ckeditor\plugin\DrupalLink.
+ */
+
+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 "drupallink" plugin.
+ *
+ * @CKEditorPlugin(
+ *   id = "drupallink",
+ *   label = @Translation("Drupal link"),
+ *   module = "ckeditor"
+ * )
+ */
+class DrupalLink extends CKEditorPluginBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFile() {
+    return drupal_get_path('module', 'ckeditor') . '/js/plugins/drupallink/plugin.js';
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getLibraries(Editor $editor) {
+    return array(
+      array('system', 'drupal.ajax'),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getConfig(Editor $editor) {
+    return array(
+      'drupalLink_dialogTitleAdd' => t('Add Link'),
+      'drupalLink_dialogTitleEdit' => t('Edit Link'),
+    );
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getButtons() {
+    $path = drupal_get_path('module', 'ckeditor') . '/js/plugins/drupallink';
+    return array(
+      'DrupalLink' => array(
+        'label' => t('Link'),
+        'image' => $path . '/link.png',
+      ),
+      'DrupalUnlink' => array(
+        'label' => t('Unlink'),
+        'image' => $path . '/unlink.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 56f9a6e..dc2c308 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/Internal.php
@@ -46,13 +46,7 @@ public function getConfig(Editor $editor) {
     $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.
-        array(0x110000 + 75, 'link'),
-        array(0x110000 + 76, NULL),
-      ),
     );
 
     // Add the allowedContent setting, which ensures CKEditor only allows tags
@@ -136,20 +130,6 @@ public function getButtons() {
         'image_alternative' => $button('redo'),
         'image_alternative_rtl' => $button('redo', 'rtl'),
       ),
-      // "link" plugin.
-      'Link' => array(
-        'label' => t('Link'),
-        'image_alternative' => $button('link'),
-      ),
-      'Unlink' => array(
-        'label' => t('Unlink'),
-        'image_alternative' => $button('unlink'),
-      ),
-      'Anchor' => array(
-        'label' => t('Anchor'),
-        'image_alternative' => $button('anchor'),
-        'image_alternative_rtl' => $button('anchor', 'rtl'),
-      ),
       // "blockquote" plugin.
       'Blockquote' => array(
         'label' => t('Blockquote'),
@@ -197,11 +177,6 @@ public function getButtons() {
         '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(
         'label' => t('Table'),
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php
index 5ab4961..12f450a 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php
@@ -25,7 +25,7 @@
 class CKEditor extends EditorBase {
 
   /**
-   * Implements \Drupal\editor\Plugin\EditPluginInterface::getDefaultSettings().
+   * {@inheritdoc}
    */
   public function getDefaultSettings() {
     return array(
@@ -33,9 +33,9 @@ public function getDefaultSettings() {
         'buttons' => array(
           array(
             'Bold', 'Italic',
-            '|', 'Link', 'Unlink',
+            '|', 'DrupalLink', 'DrupalUnlink',
             '|', 'BulletedList', 'NumberedList',
-            '|', 'Blockquote', 'Image',
+            '|', 'Blockquote', 'DrupalImage',
             '|', 'Source',
           ),
         ),
@@ -45,7 +45,7 @@ public function getDefaultSettings() {
   }
 
   /**
-   * Implements \Drupal\editor\Plugin\EditPluginInterface::settingsForm().
+   * {@inheritdoc}
    */
   public function settingsForm(array $form, array &$form_state, EditorEntity $editor) {
     $module_path = drupal_get_path('module', 'ckeditor');
@@ -134,7 +134,7 @@ public function settingsForm(array $form, array &$form_state, EditorEntity $edit
   }
 
   /**
-   * Implements \Drupal\editor\Plugin\EditPluginInterface::settingsFormSubmit().
+   * {@inheritdoc}
    */
   public function settingsFormSubmit(array $form, array &$form_state) {
     // Modify the toolbar settings by reference. The values in
@@ -151,7 +151,7 @@ public function settingsFormSubmit(array $form, array &$form_state) {
   }
 
   /**
-   * Implements \Drupal\editor\Plugin\EditPluginInterface::getJSSettings().
+   * {@inheritdoc}
    */
   public function getJSSettings(EditorEntity $editor) {
     $language_interface = language(Language::TYPE_INTERFACE);
@@ -172,6 +172,8 @@ public function getJSSettings(EditorEntity $editor) {
       'toolbar' => $this->buildToolbarJSSetting($editor),
       'contentsCss' => $this->buildContentsCssJSSetting($editor),
       'extraPlugins' => implode(',', array_keys($external_plugins)),
+      // @todo: Remove image and link plugins from CKEditor build.
+      'removePlugins' => 'image,link',
       'language' => $language_interface->langcode,
       // Configure CKEditor to not load styles.js. The StylesCombo plugin will
       // set stylesSet according to the user's settings, if the "Styles" button
@@ -192,12 +194,25 @@ public function getJSSettings(EditorEntity $editor) {
   }
 
   /**
-   * Implements \Drupal\editor\Plugin\EditPluginInterface::getLibraries().
+   * {@inheritdoc}
    */
   public function getLibraries(EditorEntity $editor) {
-    return array(
+    $libraries = array(
       array('ckeditor', 'drupal.ckeditor'),
     );
+
+    // Get the required libraries for any enabled plugins.
+    $manager = drupal_container()->get('plugin.manager.ckeditor.plugin');
+    $enabled_plugins = array_keys($manager->getEnabledPlugins($editor));
+    foreach ($enabled_plugins as $plugin_id) {
+      $plugin = $manager->createInstance($plugin_id);
+      $additional_libraries = array_udiff($plugin->getLibraries($editor), $libraries, function($a, $b) {
+        return $a[0] === $b[0] && $a[1] === $b[1] ? 0 : 1;
+      });
+      $libraries = array_merge($libraries, $additional_libraries);
+    }
+
+    return $libraries;
   }
 
   /**
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php
index 4d07526..68877f4 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorAdminTest.php
@@ -81,9 +81,9 @@ function testAdmin() {
         'buttons' => array(
           array(
             'Bold', 'Italic',
-            '|', 'Link', 'Unlink',
+            '|', 'DrupalLink', 'DrupalUnlink',
             '|', 'BulletedList', 'NumberedList',
-            '|', 'Blockquote', 'Image',
+            '|', 'Blockquote', 'DrupalImage',
             '|', 'Source',
           ),
         ),
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php
index 63a816f..6805c6e 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorLoadingTest.php
@@ -100,6 +100,7 @@ function testLoading() {
     $ckeditor_plugin = drupal_container()->get('plugin.manager.editor')->createInstance('ckeditor');
     $editor = entity_load('editor', 'filtered_html');
     $expected = array('formats' => array('filtered_html' => array(
+      'format' => 'filtered_html',
       'editor' => 'ckeditor',
       'editorSettings' => $ckeditor_plugin->getJSSettings($editor),
     )));
@@ -121,11 +122,11 @@ function testLoading() {
     module_enable(array('ckeditor_test'));
     drupal_container()->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions();
     $editor->settings['toolbar']['buttons'][0][] = 'Llama';
-    $editor->settings['plugins']['internal']['link_shortcut'] = 'CTRL+K';
     $editor->save();
     $this->drupalGet('node/add/article');
     list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck();
     $expected = array('formats' => array('filtered_html' => array(
+      'format' => 'filtered_html',
       'editor' => 'ckeditor',
       'editorSettings' => $ckeditor_plugin->getJSSettings($editor),
     )));
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php
index 8cee06e..3d31c04 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorPluginManagerTest.php
@@ -69,9 +69,13 @@ function testEnabledPlugins() {
     // 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(), $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.');
+    $this->assertIdentical(array('drupalimage', 'drupallink', 'internal', 'stylescombo'), $definitions, 'No CKEditor plugins found besides the built-in ones.');
+    $enabled_plugins = array(
+      'drupalimage' => 'core/modules/ckeditor/js/plugins/drupalimage/plugin.js',
+      'drupallink' => 'core/modules/ckeditor/js/plugins/drupallink/plugin.js',
+    );
+    $this->assertIdentical($enabled_plugins, $this->manager->getEnabledPlugins($editor), 'Only built-in plugins are enabled.');
+    $this->assertIdentical(array('internal' => NULL) + $enabled_plugins, $this->manager->getEnabledPlugins($editor, TRUE), 'Only the "internal" plugin is enabled.');
 
     // Enable the CKEditor Test module, which has the Llama plugin (plus three
     // variations of it, to cover all possible ways a plugin can be enabled) and
@@ -82,9 +86,9 @@ function testEnabledPlugins() {
     // 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(), $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.');
+    $this->assertIdentical(array('drupalimage', 'drupallink', 'internal', 'llama', 'llama_button', 'llama_contextual', 'llama_contextual_and_button', 'stylescombo'), $plugin_ids, 'Additional CKEditor plugins found.');
+    $this->assertIdentical($enabled_plugins, $this->manager->getEnabledPlugins($editor), 'Only the internal plugins are enabled.');
+    $this->assertIdentical(array('internal' => NULL) + $enabled_plugins, $this->manager->getEnabledPlugins($editor, TRUE), 'Only the "internal" plugin is enabled.');
 
     // Case 3: enable each of the newly available plugins, if possible:
     // a. Llama: cannot be enabled, since it does not implement
@@ -106,18 +110,18 @@ function testEnabledPlugins() {
     $file['b'] = 'core/modules/ckeditor/tests/modules/js/llama_button.js';
     $file['c'] = 'core/modules/ckeditor/tests/modules/js/llama_contextual.js';
     $file['cb'] = 'core/modules/ckeditor/tests/modules/js/llama_contextual_and_button.js';
-    $expected = array('llama_button' => $file['b'], 'llama_contextual_and_button' => $file['cb']);
+    $expected = $enabled_plugins + array('llama_button' => $file['b'], 'llama_contextual_and_button' => $file['cb']);
     $this->assertIdentical($expected, $this->manager->getEnabledPlugins($editor), 'The LlamaButton and LlamaContextualAndButton plugins are enabled.');
     $this->assertIdentical(array('internal' => NULL) + $expected, $this->manager->getEnabledPlugins($editor, TRUE), 'The LlamaButton and LlamaContextualAndButton plugins are enabled.');
     $editor->settings['toolbar']['buttons'][0] = $original_toolbar;
     $editor->settings['toolbar']['buttons'][0][] = 'Strike';
     $editor->save();
-    $expected = array('llama_contextual' => $file['c'], 'llama_contextual_and_button' => $file['cb']);
+    $expected = $enabled_plugins + array('llama_contextual' => $file['c'], 'llama_contextual_and_button' => $file['cb']);
     $this->assertIdentical($expected, $this->manager->getEnabledPlugins($editor), 'The  LLamaContextual and LlamaContextualAndButton plugins are enabled.');
     $this->assertIdentical(array('internal' => NULL) + $expected, $this->manager->getEnabledPlugins($editor, TRUE), 'The LlamaContextual and LlamaContextualAndButton plugins are enabled.');
     $editor->settings['toolbar']['buttons'][0][] = 'Llama';
     $editor->save();
-    $expected = array('llama_button' => $file['b'], 'llama_contextual' => $file['c'], 'llama_contextual_and_button' => $file['cb']);
+    $expected = $enabled_plugins + array('llama_button' => $file['b'], 'llama_contextual' => $file['c'], 'llama_contextual_and_button' => $file['cb']);
     $this->assertIdentical($expected, $this->manager->getEnabledPlugins($editor), 'The LlamaButton, LlamaContextual and LlamaContextualAndButton plugins are enabled.');
     $this->assertIdentical(array('internal' => NULL) + $expected, $this->manager->getEnabledPlugins($editor, TRUE), 'The LLamaButton, LlamaContextual and LlamaContextualAndButton plugins are enabled.');
   }
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
index 971d12e..978ba4a 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Tests/CKEditorTest.php
@@ -79,13 +79,21 @@ function testGetJSSettings() {
 
     // Default toolbar.
     $expected_config = $this->getDefaultInternalConfig() + array(
+      'drupalImage_dialogTitleAdd' => 'Insert Image',
+      'drupalImage_dialogTitleEdit' => 'Edit Image',
+      'drupalLink_dialogTitleAdd' => 'Add Link',
+      'drupalLink_dialogTitleEdit' => 'Edit Link',
       'allowedContent' => $this->getDefaultAllowedContentConfig(),
       'toolbar' => $this->getDefaultToolbarConfig(),
       'contentsCss' => $this->getDefaultContentsCssConfig(),
-      'extraPlugins' => '',
+      'extraPlugins' => 'drupalimage,drupallink',
+      'removePlugins' => 'image,link',
       'language' => 'en',
       'stylesSet' => FALSE,
-      'drupalExternalPlugins' => array(),
+      'drupalExternalPlugins' => array(
+        'drupalimage' => file_create_url('core/modules/ckeditor/js/plugins/drupalimage/plugin.js'),
+        'drupallink' => file_create_url('core/modules/ckeditor/js/plugins/drupallink/plugin.js'),
+      ),
     );
     ksort($expected_config);
     $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for default configuration.');
@@ -96,17 +104,16 @@ function testGetJSSettings() {
     drupal_container()->get('plugin.manager.ckeditor.plugin')->clearCachedDefinitions();
     $editor->settings['toolbar']['buttons'][0][] = 'Strike';
     $editor->settings['toolbar']['buttons'][1][] = 'Format';
-    $editor->settings['plugins']['internal']['link_shortcut'] = 'CTRL+K';
     $editor->save();
     $expected_config['toolbar'][count($expected_config['toolbar'])-2]['items'][] = 'Strike';
     $expected_config['toolbar'][]['items'][] = 'Format';
     $expected_config['toolbar'][] = '/';
     $expected_config['format_tags'] = 'p;h4;h5;h6';
-    $expected_config['extraPlugins'] = 'llama_contextual,llama_contextual_and_button';
+    $expected_config['extraPlugins'] .= ',llama_contextual,llama_contextual_and_button';
+    $expected_config['removePlugins'] = 'image,link';
     $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');
-    $expected_config['keystrokes'] = array(array(1114187, 'link'), array(1114188, NULL));
     ksort($expected_config);
     $this->assertIdentical($expected_config, $this->ckeditor->getJSSettings($editor), 'Generated JS settings are correct for customized configuration.');
 
@@ -302,9 +309,7 @@ protected function getDefaultInternalConfig() {
     return array(
       'customConfig' => '',
       'pasteFromWordPromptCleanup' => TRUE,
-      'removeDialogTabs' => 'image:Link;image:advanced;link:advanced',
       'resize_dir' => 'vertical',
-      'keystrokes' =>  array(array(0x110000 + 75, 'link'), array(0x110000 + 76, NULL)),
     );
   }
 
@@ -323,9 +328,9 @@ protected function getDefaultAllowedContentConfig() {
   protected function getDefaultToolbarConfig() {
     return array(
       0 => array('items' => array('Bold', 'Italic')),
-      1 => array('items' => array('Link', 'Unlink')),
+      1 => array('items' => array('DrupalLink', 'DrupalUnlink')),
       2 => array('items' => array('BulletedList', 'NumberedList')),
-      3 => array('items' => array('Blockquote', 'Image')),
+      3 => array('items' => array('Blockquote', 'DrupalImage')),
       4 => array('items' => array('Source')),
       5 => '/'
     );
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 0fd3adf..f8c7443 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
@@ -33,6 +33,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() {
@@ -47,7 +61,7 @@ function getFile() {
   }
 
   /**
-   * 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.css b/core/modules/editor/css/editor.css
new file mode 100644
index 0000000..e8a9d9e
--- /dev/null
+++ b/core/modules/editor/css/editor.css
@@ -0,0 +1,15 @@
+/**
+ * @file
+ * Styles for text editors.
+ */
+.editor-dialog {
+  /* This !important is required to override inline CSS of jQuery UI. */
+  width: 80% !important;
+  max-width: 500px;
+}
+
+@media screen and (max-width: 600px) {
+  .editor-dialog {
+    width: 95% !important;
+  }
+}
diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
index a0e78a6..687a47a 100644
--- a/core/modules/editor/editor.module
+++ b/core/modules/editor/editor.module
@@ -83,6 +83,9 @@ function editor_library_info() {
     'js' => array(
       $path . '/js/editor.js' => array(),
     ),
+    'css' => array(
+      $path . '/css/editor.css' => array(),
+    ),
     'dependencies' => array(
       array('system', 'jquery'),
       array('system', 'drupal'),
@@ -91,6 +94,20 @@ function editor_library_info() {
     ),
   );
 
+  $libraries['drupal.editor.dialog'] = array(
+    'title' => 'Text Editor Dialog',
+    'version' => VERSION,
+    'js' => array(
+      $path . '/js/editor.dialog.js' => array('weight' => 2),
+    ),
+    '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..f4ae496 100644
--- a/core/modules/editor/editor.routing.yml
+++ b/core/modules/editor/editor.routing.yml
@@ -5,3 +5,15 @@ editor_field_untransformed_text:
   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'
+editor_link_dialog:
+  pattern: '/editor/dialog/link/{filter_format}'
+  defaults:
+    _form: '\Drupal\editor\Form\EditorLinkDialog'
+  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..35dd4be
--- /dev/null
+++ b/core/modules/editor/js/editor.dialog.js
@@ -0,0 +1,21 @@
+/**
+ * AJAX commands used by Editor module.
+ */
+
+(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.AjaxCommands.prototype.closeDialog. Editors that are integrated
+ * with dialogs must independently listen for an editor:dialogsave event to save
+ * the changes into the contents of their interface.
+ */
+Drupal.AjaxCommands.prototype.editorDialogSave = function (ajax, response, status) {
+  $(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..a340762
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/Ajax/EditorDialogSave.php
@@ -0,0 +1,47 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\editor\Ajax\EditorDialogSave.
+ */
+
+namespace Drupal\editor\Ajax;
+
+use Drupal\Core\Ajax\CommandInterface;
+
+/**
+ * Provides an AJAX command for saving the contents of an editor dialog.
+ *
+ * This command is implemented in editor.dialog.js in
+ * Drupal.AjaxCommands.prototype.editorDialogSave.
+ */
+class EditorDialogSave implements CommandInterface {
+
+  /**
+   * An array of values that will be passed back to the editor by the dialog.
+   *
+   * @var string
+   */
+  protected $values;
+
+  /**
+   * Constructs a EditorDialogSave object.
+   *
+   * @param string $values
+   *   The values that should be passed to the form constructor in Drupal.
+   */
+  public function __construct($values) {
+    $this->values = $values;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function render() {
+    return array(
+      'command' => 'editorDialogSave',
+      'values' => $this->values,
+    );
+  }
+
+}
diff --git a/core/modules/editor/lib/Drupal/editor/EditorController.php b/core/modules/editor/lib/Drupal/editor/EditorController.php
index 2968454..93bbe52 100644
--- a/core/modules/editor/lib/Drupal/editor/EditorController.php
+++ b/core/modules/editor/lib/Drupal/editor/EditorController.php
@@ -9,8 +9,13 @@
 
 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\editor\Form\EditorImageDialog;
+use Drupal\editor\Form\EditorLinkDialog;
+use Drupal\filter\Plugin\Core\Entity\FilterFormat;
 
 /**
  * Returns responses for Editor module routes.
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..0a913bd
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php
@@ -0,0 +1,136 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\editor\Form\EditorImageDialog.
+ */
+
+namespace Drupal\editor\Form;
+
+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 an image dialog for text editors.
+ */
+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');
+    $form['#prefix'] = '<div id="editor-image-dialog-form">';
+    $form['#suffix'] = '</div>';
+
+    // Everything under the "attributes" key is merged directly into the
+    // generated 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()) {
+      unset($form['#prefix'], $form['#suffix']);
+      $output = drupal_render($form);
+      $output = '<div>' . theme('status_messages') . $output . '</div>';
+      $response->addCommand(new HtmlCommand('#editor-image-dialog-form', $output));
+    }
+    else {
+      $response->addCommand(new EditorDialogSave($form_state['values']));
+      $response->addCommand(new CloseModalDialogCommand());
+    }
+
+    return $response;
+  }
+
+}
diff --git a/core/modules/editor/lib/Drupal/editor/Form/EditorLinkDialog.php b/core/modules/editor/lib/Drupal/editor/Form/EditorLinkDialog.php
new file mode 100644
index 0000000..ce2b5c7
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/Form/EditorLinkDialog.php
@@ -0,0 +1,103 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\editor\Form\EditorLinkDialog.
+ */
+
+namespace Drupal\editor\Form;
+
+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 link dialog for text editors.
+ */
+class EditorLinkDialog implements FormInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getFormID() {
+    return 'editor_link_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');
+    $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' => t('URL'),
+      '#type' => 'textfield',
+      '#default_value' => isset($input['href']) ? $input['href'] : '',
+      '#maxlength' => 2048,
+    );
+
+    $form['attributes']['target'] = array(
+      '#title' => t('Open in new window'),
+      '#type' => 'checkbox',
+      '#default_value' => !empty($input['target']),
+      '#return_value' => '_blank',
+    );
+
+    $form['actions'] = array(
+      '#type' => 'actions',
+    );
+    $form['actions']['save_modal'] = array(
+      '#type' => 'submit',
+      '#value' => 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()) {
+      unset($form['#prefix'], $form['#suffix']);
+      $output = drupal_render($form);
+      $output = '<div>' . theme('status_messages') . $output . '</div>';
+      $response->addCommand(new HtmlCommand('#editor-link-dialog-form', $output));
+    }
+    else {
+      $response->addCommand(new EditorDialogSave($form_state['values']));
+      $response->addCommand(new CloseModalDialogCommand());
+    }
+
+    return $response;
+  }
+
+}
diff --git a/core/modules/editor/lib/Drupal/editor/Plugin/EditorManager.php b/core/modules/editor/lib/Drupal/editor/Plugin/EditorManager.php
index 4014d0c..585b19f 100644
--- a/core/modules/editor/lib/Drupal/editor/Plugin/EditorManager.php
+++ b/core/modules/editor/lib/Drupal/editor/Plugin/EditorManager.php
@@ -76,6 +76,7 @@ public function getAttachments(array $format_ids) {
 
       // JavaScript settings.
       $settings[$format_id] = array(
+        'format' => $format_id,
         'editor' => $editor->editor,
         'editorSettings' => $plugin->getJSSettings($editor),
       );
diff --git a/core/modules/editor/lib/Drupal/editor/Tests/EditorLoadingTest.php b/core/modules/editor/lib/Drupal/editor/Tests/EditorLoadingTest.php
index c0731cc..8de3610 100644
--- a/core/modules/editor/lib/Drupal/editor/Tests/EditorLoadingTest.php
+++ b/core/modules/editor/lib/Drupal/editor/Tests/EditorLoadingTest.php
@@ -93,6 +93,7 @@ function testLoading() {
     $this->drupalGet('node/add/article');
     list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck();
     $expected = array('formats' => array('full_html' => array(
+      'format' => 'full_html',
       'editor' => 'unicorn',
       'editorSettings' => array('ponyModeEnabled' => TRUE),
     )));
@@ -119,6 +120,7 @@ function testLoading() {
     $this->drupalGet('node/add/article');
     list($settings, $editor_settings_present, $editor_js_present, $body, $format_selector) = $this->getThingsToCheck();
     $expected = array('formats' => array('plain_text' => array(
+      'format' => 'plain_text',
       'editor' => 'unicorn',
       'editorSettings' => array('ponyModeEnabled' => TRUE),
     )));
diff --git a/core/modules/editor/lib/Drupal/editor/Tests/EditorManagerTest.php b/core/modules/editor/lib/Drupal/editor/Tests/EditorManagerTest.php
index cc8e3c0..729c93b 100644
--- a/core/modules/editor/lib/Drupal/editor/Tests/EditorManagerTest.php
+++ b/core/modules/editor/lib/Drupal/editor/Tests/EditorManagerTest.php
@@ -102,6 +102,7 @@ function testManager() {
           'type' => 'setting',
           'data' => array('editor' => array('formats' => array(
             'full_html' => array(
+              'format'  => 'full_html',
               'editor' => 'unicorn',
               'editorSettings' => $unicorn_plugin->getJSSettings($editor),
             )
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 7a1b8d4..ab9ac56 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1234,9 +1234,13 @@ function system_library_info() {
     'js' => array(
       'core/misc/dialog.js' => array('group' => JS_LIBRARY),
     ),
+    'css' => array(
+      'core/misc/dialog.theme.css' => array('weight' => 1),
+    ),
     'dependencies' => array(
       array('system', 'jquery'),
       array('system', 'drupal'),
+      array('system', 'drupal.debounce'),
       array('system', 'drupalSettings'),
       array('system', 'jquery.ui.dialog')
     ),
diff --git a/core/profiles/standard/config/editor.editor.basic_html.yml b/core/profiles/standard/config/editor.editor.basic_html.yml
index e66cc2e..cf4a600 100644
--- a/core/profiles/standard/config/editor.editor.basic_html.yml
+++ b/core/profiles/standard/config/editor.editor.basic_html.yml
@@ -7,14 +7,14 @@ settings:
         - Bold
         - Italic
         - '|'
-        - Link
-        - Unlink
+        - DrupalLink
+        - DrupalUnlink
         - '|'
         - BulletedList
         - NumberedList
         - '|'
         - Blockquote
-        - Image
+        - DrupalImage
         - '|'
         - Source
   plugins:
diff --git a/core/profiles/standard/config/editor.editor.full_html.yml b/core/profiles/standard/config/editor.editor.full_html.yml
index 378cc45..c0111f9 100644
--- a/core/profiles/standard/config/editor.editor.full_html.yml
+++ b/core/profiles/standard/config/editor.editor.full_html.yml
@@ -12,14 +12,14 @@ settings:
         - -
         - RemoveFormat
         - '|'
-        - Link
-        - Unlink
+        - DrupalLink
+        - DrupalUnlink
         - '|'
         - BulletedList
         - NumberedList
         - '|'
         - Blockquote
-        - Image
+        - DrupalImage
         - Table
         - HorizontalRule
         - '|'
diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css
index a7a36a8..9a05bb1 100644
--- a/core/themes/bartik/css/style.css
+++ b/core/themes/bartik/css/style.css
@@ -109,7 +109,7 @@ pre {
 
 body,
 #site-slogan,
-.ui-widget,
+#page .ui-widget,
 .comment-form label,
 .node-form label,
 .node-form .description {
diff --git a/core/themes/seven/jquery.ui.theme.css b/core/themes/seven/jquery.ui.theme.css
index ee72497..4a171fb 100644
--- a/core/themes/seven/jquery.ui.theme.css
+++ b/core/themes/seven/jquery.ui.theme.css
@@ -330,55 +330,6 @@
   opacity: .70;
   filter: Alpha(Opacity=70);
 }
-
-/**
- * Dialogs
- */
-.ui-dialog {
-  background: #fff;
-  border: solid 1px #ccc;
-  padding: 0;
-}
-.ui-dialog .ui-dialog-titlebar {
-  font-weight: bold;
-  background: #e1e2dc;
-}
-.ui-dialog .ui-dialog-buttonpane {
-  border-width: 0;
-}
-.ui-dialog .ui-dialog-buttonpane button {
-  cursor: pointer;
-  padding: 4px 17px;
-  color: #5a5a5a;
-  text-align: center;
-  font-family: "Lucida Grande", Verdana, sans-serif;
-  font-weight: normal;
-  font-size: 1em;
-  border: 1px solid #e4e4e4;
-  border-bottom: 1px solid #b4b4b4;
-  border-left-color: #D2D2D2;
-  border-right-color: #D2D2D2;
-  background: url(images/buttons.png) 0 0 repeat-x;
-  border-radius: 20px;
-}
-.ui-dialog .ui-dialog-buttonpane button:hover,
-.ui-dialog .ui-dialog-buttonpane button:focus {
-  background-position: 0 -40px;
-  border: 1px solid #bebebe;
-  border-left-color: #afafaf;
-  border-right-color: #afafaf;
-  border-bottom-color: #9b9b9b;
-  color: #2e2e2e;
-}
-.ui-dialog .ui-dialog-buttonpane button:active {
-  background-position: 0 -80px;
-  border: 1px solid #333;
-  border-left-color: #222;
-  border-right-color: #222;
-  border-bottom-color: #111;
-  color: #fff;
-  text-shadow: #222 0px -1px 0px;
-}
 .overlay {
   padding-right: 26px;
 }
