commit 0bdb5835de7b354eb871bd76fc755eac2ba9892e
Author: Nathan Haug <nate@quicksketch.org>
Date:   Mon Jul 22 16:10:46 2013 -0700

    #30

diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module
index 4ca9956..a478d27 100644
--- a/core/modules/ckeditor/ckeditor.module
+++ b/core/modules/ckeditor/ckeditor.module
@@ -55,6 +55,20 @@ function ckeditor_library_info() {
       array('system', 'underscore')
     ),
   );
+  $libraries['drupal.ckeditor.drupalimage.admin'] = array(
+    'title' => 'Only show the "drupalimage" plugin settings when its button is enabled.',
+    'version' => VERSION,
+    'js' => array(
+      $module_path . '/js/ckeditor.drupalimage.admin.js' => array(),
+    ),
+    'dependencies' => array(
+      array('system', 'jquery'),
+      array('system', 'drupal'),
+      array('system', 'jquery.once'),
+      array('system', 'drupal.vertical-tabs'),
+      array('system', 'drupalSettings'),
+    ),
+  );
   $libraries['drupal.ckeditor.stylescombo.admin'] = array(
     'title' => 'Only show the "stylescombo" plugin settings when its button is enabled.',
     'version' => VERSION,
diff --git a/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js b/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
new file mode 100644
index 0000000..01d01c7
--- /dev/null
+++ b/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
@@ -0,0 +1,59 @@
+(function ($, Drupal, drupalSettings) {
+
+"use strict";
+
+/**
+ * Shows the "drupalimage" plugin settings only when the button is enabled.
+ */
+Drupal.behaviors.ckeditorDrupalImageSettings = {
+  attach: function (context) {
+    var $context = $(context);
+    var $drupalImageVerticalTab = $('#edit-editor-settings-plugins-drupalimage').data('verticalTab');
+
+    // Hide if the "DrupalImage" button is disabled.
+    if ($('.ckeditor-toolbar-disabled li[data-button-name="DrupalImage"]').length === 1) {
+      $drupalImageVerticalTab.tabHide();
+    }
+
+    // React to added/removed toolbar buttons.
+    $context
+      .find('.ckeditor-toolbar-active')
+      .on('CKEditorToolbarChanged.ckeditorDrupalImageSettings', function (e, action, button) {
+        if (button === 'DrupalImage') {
+          if (action === 'added') {
+            $drupalImageVerticalTab.tabShow();
+          }
+          else {
+            $drupalImageVerticalTab.tabHide();
+          }
+        }
+      });
+  }
+
+};
+
+/**
+ * Provides the summary for the "drupalimage" plugin settings vertical tab.
+ */
+Drupal.behaviors.ckeditorDrupalImageSettingsSummary = {
+  attach: function () {
+    $('#edit-editor-settings-plugins-drupalimage').drupalSetSummary(function (context) {
+      var $maxFileSize = $('input[name="editor[settings][plugins][drupalimage][max_size]"]');
+      var $maxWidth = $('input[name="editor[settings][plugins][drupalimage][max_dimensions][width]"]');
+      var $maxHeight = $('input[name="editor[settings][plugins][drupalimage][max_dimensions][height]"]');
+      var $scheme = $('input[name="editor[settings][plugins][drupalimage][scheme]"]:checked');
+
+      var maxFileSize = $maxFileSize.val() ? $maxFileSize.val() : $maxFileSize.attr('placeholder');
+      var maxDimensions = ($maxWidth.val() && $maxHeight.val()) ? '(' + $maxWidth.val() + 'x' + $maxHeight.val() + ')' : '';
+
+      var output = '';
+      output += Drupal.t('Max image size: @size @dimensions', { '@size': maxFileSize, '@dimensions': maxDimensions });
+      if ($scheme.length) {
+        output += '<br />' + $scheme.attr('data-label');
+      }
+      return output;
+    });
+  }
+};
+
+})(jQuery, Drupal, drupalSettings);
diff --git a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
index 3dd92c4..ecacb4a 100644
--- a/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
+++ b/core/modules/ckeditor/js/plugins/drupalimage/plugin.js
@@ -72,6 +72,12 @@ CKEDITOR.plugins.add('drupalimage', {
           dialogClass: 'editor-image-dialog'
         };
 
+        // Add upload-image field based on the textarea's "data-image-uploads"
+        // attribute.
+        if (editor.element.$.attributes['data-editor-uploads']) {
+          existingValues['uploadsEnabled'] = 1;
+        }
+
         // Open the dialog for the edit form.
         Drupal.ckeditor.openDialog(editor, Drupal.url('editor/dialog/image/' + editor.config.drupal.format), existingValues, saveCallback, dialogSettings);
       }
diff --git a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php
index 84c31b2..189d522 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php
@@ -8,6 +8,7 @@
 namespace Drupal\ckeditor\Plugin\CKEditorPlugin;
 
 use Drupal\ckeditor\CKEditorPluginBase;
+use Drupal\ckeditor\CKEditorPluginConfigurableInterface;
 use Drupal\ckeditor\Annotation\CKEditorPlugin;
 use Drupal\Core\Annotation\Translation;
 use Drupal\editor\Plugin\Core\Entity\Editor;
@@ -21,7 +22,7 @@
  *   module = "ckeditor"
  * )
  */
-class DrupalImage extends CKEditorPluginBase {
+class DrupalImage extends CKEditorPluginBase implements CKEditorPluginConfigurableInterface {
 
   /**
    * {@inheritdoc}
@@ -61,4 +62,108 @@ public function getButtons() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function settingsForm(array $form, array &$form_state, Editor $editor) {
+    // Defaults.
+    $editor->settings['image_upload'] = isset($editor->settings['image_upload']) ? $editor->settings['image_upload'] : array();
+    $editor->settings['image_upload'] += array(
+      'scheme' => file_default_scheme(),
+      'directory' => 'inline-images',
+      'max_size' => '',
+      'max_dimensions' => array('width' => '', 'height' => ''),
+    );
+
+    // Validate all settings as a group.
+    $form['#element_validate'] = array(
+      array($this, 'validateImageUploadValues'),
+    );
+    $form['#attached'] = array(
+      'library' => array(array('ckeditor', 'drupal.ckeditor.drupalimage.admin')),
+    );
+
+    $default_max_size = format_size(file_upload_max_size());
+    $form['max_size'] = array(
+      '#type' => 'textfield',
+      '#default_value' => $editor->settings['image_upload']['max_size'],
+      '#title' => t('Maximum file size'),
+      '#description' => t('If this is left empty, then the file size will be limited by the PHP maximum upload size of @size', array('@size' => $default_max_size)),
+      '#maxlength' => 20,
+      '#size' => 10,
+      '#placeholder' => $default_max_size,
+    );
+
+    $form['max_dimensions'] = array(
+      '#type' => 'item',
+      '#title' => t('Maximum dimensions'),
+      '#field_prefix' => '<div class="container-inline clearfix">',
+      '#field_suffix' => '</div>',
+      '#description' => t('Images larger than these dimensions will be scaled down.'),
+    );
+    $form['max_dimensions']['width'] = array(
+      '#title' => t('Width'),
+      '#title_display' => 'invisible',
+      '#type' => 'number',
+      '#default_value' => $editor->settings['image_upload']['max_dimensions']['width'],
+      '#size' => 8,
+      '#maxlength' => 8,
+      '#min' => 1,
+      '#max' => 99999,
+      '#placeholder' => 'width',
+      '#field_suffix' => ' x ',
+    );
+    $form['max_dimensions']['height'] = array(
+      '#title' => t('Height'),
+      '#title_display' => 'invisible',
+      '#type' => 'number',
+      '#default_value' => $editor->settings['image_upload']['max_dimensions']['height'],
+      '#size' => 8,
+      '#maxlength' => 8,
+      '#min' => 1,
+      '#max' => 99999,
+      '#placeholder' => 'height',
+      '#field_suffix' => 'pixels',
+    );
+
+    // Any visible, writable wrapper can potentially be used for uploads, 
+    // including a remote file system that integrates with a CDN.
+    $stream_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_WRITE_VISIBLE);
+    foreach ($stream_wrappers as $scheme => $info) {
+      $options[$scheme] = $info['description'];
+    }
+
+    $config = config('system.file');
+    if (!empty($options)) {
+      $form['scheme'] = array(
+        '#type' => 'radios',
+        '#title' => t('File storage scheme'),
+        '#default_value' => $editor->settings['image_upload']['scheme'],
+        '#options' => $options,
+        '#access' => count($options) > 1,
+      );
+    }
+
+    foreach ($stream_wrappers as $scheme => $info) {
+      $form['scheme'][$scheme]['#attributes']['data-label'] = t('Storage: @name', array('@name' => $info['name']));
+    }
+
+    $form['directory'] = array(
+      '#type' => 'textfield',
+      '#default_value' => $editor->settings['image_upload']['directory'],
+      '#title' => t('Upload directory'),
+      '#description' => t('A directory relative to Drupal\'s files directory where upload images will be stored.'),
+    );
+
+    return $form;
+  }
+
+  function validateImageUploadValues(array $element, array &$form_state) {
+    $settings = &$form_state['values']['editor']['settings']['plugins']['drupalimage'];
+
+    // Save the settings at the editor level, not the plugin.
+    $form_state['values']['editor']['settings']['image_upload'] = $settings;
+    unset($form_state['values']['editor']['settings']['plugins']['drupalimage']);
+  }
+
 }
diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
index af2b8ea..eb97e0f 100644
--- a/core/modules/editor/editor.module
+++ b/core/modules/editor/editor.module
@@ -8,6 +8,7 @@
 use Drupal\file\Plugin\Core\Entity\File;
 use Drupal\editor\Plugin\Core\Entity\Editor;
 use Drupal\Component\Utility\NestedArray;
+use Drupal\system\SystemConfigFormBase;
 
 /**
  * Implements hook_help().
@@ -55,6 +56,7 @@ function editor_menu_alter(&$items) {
  */
 function editor_element_info() {
   $type['text_format'] = array(
+    '#editor_uploads' => FALSE,
     '#pre_render' => array('editor_pre_render_format'),
   );
   return $type;
@@ -375,6 +377,11 @@ function editor_pre_render_format($element) {
     $element['format']['format']['#attributes']['data-editor-for'] = $field_id;
   }
 
+  // Set a data attribute if uploads are enabled.
+  if ($element['#editor_uploads']) {
+    $element['value']['#attributes']['data-editor-uploads'] = 'true';
+  }
+
   // Attach Text Editor module's (this module) library.
   $element['#attached']['library'][] = array('editor', 'drupal.editor');
 
diff --git a/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php b/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php
index 0fa6249..4e600c8 100644
--- a/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php
+++ b/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php
@@ -13,6 +13,8 @@
 use Drupal\Core\Ajax\HtmlCommand;
 use Drupal\editor\Ajax\EditorDialogSave;
 use Drupal\Core\Ajax\CloseModalDialogCommand;
+use Drupal\Core\StreamWrapper\LocalStream;
+use Drupal\file\FileInterface;
 
 /**
  * Provides an image dialog for text editors.
@@ -42,16 +44,54 @@ public function buildForm(array $form, array &$form_state, FilterFormat $filter_
     $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,
+    $editor = editor_load($filter_format->format);
+
+    // Construct strings to use in the upload validators.
+    if (!empty($editor->settings['image_upload']['dimensions'])) {
+      $max_dimensions = $editor->settings['image_upload']['dimensions']['max_width'] . 'x' . $editor->settings['image_upload']['dimensions']['max_height'];
+    }
+    else {
+      $max_dimensions = 0;
+    }
+    $max_filesize = min(parse_size($editor->settings['image_upload']['max_size']), file_upload_max_size());
+
+    $existing_file = isset($input['data-file-uuid']) ? entity_load_by_uuid('file', $input['data-file-uuid']) : NULL;
+    $fid = $existing_file ? $existing_file->id() : NULL;
+
+    $form['fid'] = array(
+      '#title' => t('Image'),
+      '#type' => 'managed_file',
+      '#upload_location' => $editor->settings['image_upload']['scheme'] . '://' .$editor->settings['image_upload']['directory'],
+      '#default_value' => $fid ? array($fid) : NULL,
+      '#upload_validators' => array(
+        'file_validate_extensions' => array('gif png jpg jpeg'),
+        'file_validate_size' => array($max_filesize),
+        'file_validate_image_resolution' => array($max_dimensions),
+      ),
       '#required' => TRUE,
     );
 
+    $form['attributes']['src'] = array(
+     '#title' => t('URL'),
+     '#type' => 'textfield',
+     '#default_value' => isset($input['src']) ? $input['src'] : '',
+     '#maxlength' => 2048,
+     '#required' => TRUE,
+    );
+
+    // If the editor has uploads enabled and the field was requested, show
+    // an upload field. Otherwise only a plain "src" field is shown.
+    $uploads_enabled = !empty($form_state['uploads_enabled']) || !empty($input['uploadsEnabled']);
+    if ($uploads_enabled && !empty($editor->settings['image_upload'])) {
+      $form['attributes']['src']['#access'] = FALSE;
+      // Set a flag in form state so the upload field stays accessible during
+      // the validation and submit handling.
+      $form_state['uploads_enabled'] = TRUE;
+    }
+    else {
+      $form['fid']['#access'] = FALSE;
+    }
+
     $form['attributes']['alt'] = array(
       '#title' => t('Alternative text'),
       '#type' => 'textfield',
@@ -120,6 +160,15 @@ public function validateForm(array &$form, array &$form_state) {
   public function submitForm(array &$form, array &$form_state) {
     $response = new AjaxResponse();
 
+    // Convert any uploaded files from the FID values to data-uuid attributes.
+    if (!empty($form_state['values']['fid'][0])) {
+      $file = file_load($form_state['values']['fid'][0]);
+      $uri = $file->getFileUri();
+      $stream = file_stream_wrapper_get_instance_by_uri($uri);
+      $form_state['values']['attributes']['src'] = str_replace($_SERVER['DOCUMENT_ROOT'], '', $stream->realpath());
+      $form_state['values']['attributes']['data-file-uuid'] = $file->uuid();
+    }
+
     if (form_get_errors()) {
       unset($form['#prefix'], $form['#suffix']);
       $output = drupal_render($form);
diff --git a/core/modules/file/lib/Drupal/file/FileStorageController.php b/core/modules/file/lib/Drupal/file/FileStorageController.php
index 10ba8d3..f3167a6 100644
--- a/core/modules/file/lib/Drupal/file/FileStorageController.php
+++ b/core/modules/file/lib/Drupal/file/FileStorageController.php
@@ -52,7 +52,7 @@ public function baseFieldDefinitions() {
     $properties['uuid'] = array(
       'label' => t('UUID'),
       'description' => t('The file UUID.'),
-      'type' => 'string_field',
+      'type' => 'uuid_field',
       'read-only' => TRUE,
     );
     $properties['langcode'] = array(
diff --git a/core/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWidget.php b/core/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWidget.php
index b15ea69..b61fc39 100644
--- a/core/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWidget.php
+++ b/core/modules/text/lib/Drupal/text/Plugin/field/widget/TextareaWidget.php
@@ -80,6 +80,10 @@ public function formElement(array $items, $delta, array $element, $langcode, arr
       $element['#type'] = 'text_format';
       $element['#format'] = isset($items[$delta]['format']) ? $items[$delta]['format'] : NULL;
       $element['#base_type'] = $main_widget['#type'];
+
+      if (module_exists('editor')) {
+        $element['#editor_uploads'] = TRUE;
+      }
     }
     else {
       $element['value'] = $main_widget;
diff --git a/core/modules/text/lib/Drupal/text/TextProcessed.php b/core/modules/text/lib/Drupal/text/TextProcessed.php
index 9a1910f..82a4c2d 100644
--- a/core/modules/text/lib/Drupal/text/TextProcessed.php
+++ b/core/modules/text/lib/Drupal/text/TextProcessed.php
@@ -82,10 +82,50 @@ public function getValue($langcode = NULL) {
    * Implements \Drupal\Core\TypedData\TypedDataInterface::setValue().
    */
   public function setValue($value, $notify = TRUE) {
-    if (isset($value)) {
+
+    // @todo: We don't have enough context here to adequately determine if files
+    // have been removed from an current revision. Likewise we don't have the
+    // ability to decrement a usage when a revision is deleted. Switch to using
+    // hooks for hook_entity_insert/update/delete/revision_delete()?
+    if (isset($value) && module_exists('editor')) {
+      // Parse the value for any file usages and increment/decrement them as
+      // needed.
+      $field = $this->parent->getParent();
+      $entity = $field->getParent();
+      $instance = field_info_instance($entity->entityType(), $field->getName(), $entity->bundle());
+
+      $uuids = parseFileUuids($value);
+      $xpath = new \DOMXPath($dom);
+      foreach ($xpath->query('//*[@data-file-uuid]') as $node) {
+        $file_uuid = $node->getAttribute('data-file-uuid');
+        $file = entity_load_by_uuid('file', $file_uuid);
+        if ($file->status !== FILE_STATUS_PERMANENT) {
+          $file->status = FILE_STATUS_PERMANENT;
+          $file->save();
+        }
+        file_usage()->add($file, 'file', $entity->entityType(), $entity->id());
+      }
+
       // @todo This is triggered from DatabaseStorageController::invokeFieldMethod()
       // in the case of case of non-NG entity types.
       // throw new ReadOnlyException('Unable to set a computed property.');
     }
   }
+
+  /**
+   * Parse text for any data-file-uuid attributes.
+   *
+   * @param string $value
+   *   The string of data being saved in this field.
+   * @return array
+   *   An array of all found UUIDs.
+   */
+  protected function parseFileUuids($value) {
+    $dom = filter_dom_load($value);
+    $xpath = new \DOMXPath($dom);
+    $uuids = array();
+    foreach ($xpath->query('//*[@data-file-uuid]') as $node) {
+      $uuids[] = $node->getAttribute('data-file-uuid');
+    }
+  }
 }
