From 5429044d5edf7527b6de311e218cf42934f1a446 Mon Sep 17 00:00:00 2001
From: webchick <webchick@24967.no-reply.drupal.org>
Date: Thu, 29 Aug 2013 04:27:41 -0700
Subject: [PATCH] Issue #1932652 by Wim Leers, nirbhasa, quicksketch: Add image
 uploading to WYSIWYGs through editor.module.

---
 core/includes/entity.api.php                  |  12 ++
 core/modules/ckeditor/ckeditor.module         |  16 +-
 .../ckeditor/js/ckeditor.drupalimage.admin.js |  65 +++++++
 .../Plugin/CKEditorPlugin/DrupalImage.php     |  37 +++-
 .../ckeditor/Plugin/Editor/CKEditor.php       |   1 +
 .../editor/config/schema/editor.schema.yml    |  26 +++
 core/modules/editor/editor.admin.inc          | 131 +++++++++++++
 core/modules/editor/editor.module             | 181 ++++++++++++++++++
 .../lib/Drupal/editor/Entity/Editor.php       |   9 +-
 .../Drupal/editor/Form/EditorImageDialog.php  |  58 +++++-
 .../editor/Tests/EditorFileUsageTest.php      | 117 +++++++++++
 .../text/lib/Drupal/text/TextProcessed.php    |   1 +
 .../Drupal/views/Plugin/views/area/Text.php   |   2 +-
 .../views/exposed_form/InputRequired.php      |   2 +-
 .../config/editor.editor.basic_html.yml       |   8 +
 .../config/editor.editor.full_html.yml        |   8 +
 16 files changed, 660 insertions(+), 14 deletions(-)
 create mode 100644 core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
 create mode 100644 core/modules/editor/editor.admin.inc
 create mode 100644 core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php

diff --git a/core/includes/entity.api.php b/core/includes/entity.api.php
index da9c1b8255ea..8e60f00c3bef 100644
--- a/core/includes/entity.api.php
+++ b/core/includes/entity.api.php
@@ -314,6 +314,18 @@ function hook_entity_delete(Drupal\Core\Entity\EntityInterface $entity) {
     ->execute();
 }
 
+/**
+ * Respond to entity revision deletion.
+ *
+ * This hook runs after the entity type-specific revision delete hook.
+ *
+ * @param Drupal\Core\Entity\EntityInterface $entity
+ *   The entity object for the entity revision that has been deleted.
+ */
+function hook_entity_revision_delete(Drupal\Core\Entity\EntityInterface $entity) {
+  // @todo: code example
+}
+
 /**
  * Alter or execute an Drupal\Core\Entity\Query\EntityQueryInterface.
  *
diff --git a/core/modules/ckeditor/ckeditor.module b/core/modules/ckeditor/ckeditor.module
index 66a81f20d7ec..78135813fbb3 100755
--- a/core/modules/ckeditor/ckeditor.module
+++ b/core/modules/ckeditor/ckeditor.module
@@ -79,6 +79,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,
@@ -91,8 +105,6 @@ function ckeditor_library_info() {
       array('system', 'jquery.once'),
       array('system', 'drupal.vertical-tabs'),
       array('system', 'drupalSettings'),
-      // @todo D8 formUpdated event should be debounced already.
-      array('system', 'drupal.debounce'),
     ),
   );
   $libraries['ckeditor'] = array(
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 000000000000..ec093abc5fcf
--- /dev/null
+++ b/core/modules/ckeditor/js/ckeditor.drupalimage.admin.js
@@ -0,0 +1,65 @@
+(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 root = 'input[name="editor[settings][plugins][drupalimage][image_upload]';
+      var $status = $(root + '[status]"]');
+      var $maxFileSize = $(root + '[max_size]"]');
+      var $maxWidth = $(root + '[max_dimensions][width]"]');
+      var $maxHeight = $(root + '[max_dimensions][height]"]');
+      var $scheme = $(root + '[scheme]"]:checked');
+
+      var maxFileSize = $maxFileSize.val() ? $maxFileSize.val() : $maxFileSize.attr('placeholder');
+      var maxDimensions = ($maxWidth.val() && $maxHeight.val()) ? '(' + $maxWidth.val() + 'x' + $maxHeight.val() + ')' : '';
+
+      if (!$status.is(':checked')) {
+        return Drupal.t('Uploads disabled');
+      }
+
+      var output = '';
+      output += Drupal.t('Uploads enabled, max 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/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/CKEditorPlugin/DrupalImage.php
index c1e2f94493b9..3c1cde8882b3 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\Entity\Editor;
@@ -17,11 +18,11 @@
  *
  * @CKEditorPlugin(
  *   id = "drupalimage",
- *   label = @Translation("Drupal image"),
+ *   label = @Translation("Image"),
  *   module = "ckeditor"
  * )
  */
-class DrupalImage extends CKEditorPluginBase {
+class DrupalImage extends CKEditorPluginBase implements CKEditorPluginConfigurableInterface {
 
   /**
    * {@inheritdoc}
@@ -61,4 +62,36 @@ public function getButtons() {
     );
   }
 
+  /**
+   * {@inheritdoc}
+   *
+   * @see \Drupal\editor\Form\EditorImageDialog
+   * @see editor_image_upload_settings_form()
+   */
+  public function settingsForm(array $form, array &$form_state, Editor $editor) {
+    form_load_include($form_state, 'inc', 'editor', 'editor.admin');
+    $form['image_upload'] = editor_image_upload_settings_form($editor);
+    $form['image_upload']['#attached']['library'][] = array('ckeditor', 'drupal.ckeditor.drupalimage.admin');
+    $form['image_upload']['#element_validate'] = array(
+      array($this, 'validateImageUploadSettings'),
+    );
+
+    return $form;
+  }
+
+  /**
+   * #element_validate handler for the "image_upload" element in settingsForm().
+   *
+   * Moves the text editor's image upload settings from the DrupalImage plugin's
+   * own settings into $editor->image_upload.
+   *
+   * @see \Drupal\editor\Form\EditorImageDialog
+   * @see editor_image_upload_settings_form()
+   */
+  function validateImageUploadSettings(array $element, array &$form_state) {
+    $settings = &$form_state['values']['editor']['settings']['plugins']['drupalimage']['image_upload'];
+    $form_state['editor']->image_upload = $settings;
+    unset($form_state['values']['editor']['settings']['plugins']['drupalimage']);
+  }
+
 }
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 1faaa0506fa8..fb8181ab5551 100644
--- a/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php
+++ b/core/modules/ckeditor/lib/Drupal/ckeditor/Plugin/Editor/CKEditor.php
@@ -112,6 +112,7 @@ public function settingsForm(array $form, array &$form_state, EditorEntity $edit
     // CKEditor plugin settings, if any.
     $form['plugin_settings'] = array(
       '#type' => 'vertical_tabs',
+      '#title' => t('CKEditor plugin settings'),
     );
     $this->ckeditorPluginManager->injectPluginSettingsForm($form, $form_state, $editor);
     if (count(element_children($form['plugins'])) === 0) {
diff --git a/core/modules/editor/config/schema/editor.schema.yml b/core/modules/editor/config/schema/editor.schema.yml
index 6654b88bd7db..4e2c98533f8f 100644
--- a/core/modules/editor/config/schema/editor.schema.yml
+++ b/core/modules/editor/config/schema/editor.schema.yml
@@ -12,6 +12,32 @@ editor.editor.*:
       label: 'Text editor'
     settings:
       type: editor.settings.[%parent.editor]
+    image_upload:
+      type: mapping
+      label: 'Image upload settings'
+      mapping:
+        status:
+          type: boolean
+          label: 'Status'
+        scheme:
+          type: string
+          label: 'File storage'
+        directory:
+          type: string
+          label: 'Upload directory'
+        max_size:
+          type: string
+          label: 'Maximum file size'
+        max_dimensions:
+          type: mapping
+          label: 'Maximum dimensions'
+          mapping:
+            width:
+              type: integer
+              label: 'Maximum width'
+            height:
+              type: integer
+              label: 'Maximum height'
     status:
       type: boolean
       label: 'Status'
diff --git a/core/modules/editor/editor.admin.inc b/core/modules/editor/editor.admin.inc
new file mode 100644
index 000000000000..9dc6a3046457
--- /dev/null
+++ b/core/modules/editor/editor.admin.inc
@@ -0,0 +1,131 @@
+<?php
+
+/**
+ * @file
+ * Administration functions for editor.module.
+ */
+
+use Drupal\editor\Entity\Editor;
+
+/**
+ * Subform constructor to configure the text editor's image upload settings.
+ *
+ * Each text editor plugin that is configured to offer the ability to insert
+ * images and uses EditorImageDialog for that, should use this form to update
+ * the text editor's configuration so that EditorImageDialog knows whether it
+ * should allow the user to upload images.
+ *
+ * @param \Drupal\editor\Entity\Editor $editor
+ *   The text editor entity that is being edited.
+ *
+ * @return array
+ *   The image upload settings form.
+ *
+ * @see \Drupal\editor\Form\EditorImageDialog
+ * @ingroup forms
+ */
+function editor_image_upload_settings_form(Editor $editor) {
+  // Defaults.
+  $editor->image_upload = isset($editor->image_upload) ? $editor->image_upload : array();
+  $editor->image_upload += array(
+    'status' => FALSE,
+    'scheme' => file_default_scheme(),
+    'directory' => 'inline-images',
+    'max_size' => '',
+    'max_dimensions' => array('width' => '', 'height' => ''),
+  );
+
+  $form['status'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Enable image uploads'),
+    '#default_value' => $editor->image_upload['status'],
+    '#attributes' => array(
+      'data-editor-image-upload' => 'status',
+    ),
+  );
+  $show_if_image_uploads_enabled = array(
+    'visible' => array(
+      ':input[data-editor-image-upload="status"]' => array('checked' => TRUE),
+    ),
+  );
+
+  // 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'];
+  }
+  if (!empty($options)) {
+    $form['scheme'] = array(
+      '#type' => 'radios',
+      '#title' => t('File storage'),
+      '#default_value' => $editor->image_upload['scheme'],
+      '#options' => $options,
+      '#states' => $show_if_image_uploads_enabled,
+      '#access' => count($options) > 1,
+    );
+  }
+  // Set data- attributes with human-readable names for all possible stream
+  // wrappers, so that drupal.ckeditor.drupalimage.admin's summary rendering
+  // can use that.
+  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->image_upload['directory'],
+    '#title' => t('Upload directory'),
+    '#description' => t("A directory relative to Drupal's files directory where uploaded images will be stored."),
+    '#states' => $show_if_image_uploads_enabled,
+  );
+
+  $default_max_size = format_size(file_upload_max_size());
+  $form['max_size'] = array(
+    '#type' => 'textfield',
+    '#default_value' => $editor->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,
+    '#states' => $show_if_image_uploads_enabled,
+  );
+
+  $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.'),
+    '#states' => $show_if_image_uploads_enabled,
+  );
+  $form['max_dimensions']['width'] = array(
+    '#title' => t('Width'),
+    '#title_display' => 'invisible',
+    '#type' => 'number',
+    '#default_value' => $editor->image_upload['max_dimensions']['width'],
+    '#size' => 8,
+    '#maxlength' => 8,
+    '#min' => 1,
+    '#max' => 99999,
+    '#placeholder' => 'width',
+    '#field_suffix' => ' x ',
+    '#states' => $show_if_image_uploads_enabled,
+  );
+  $form['max_dimensions']['height'] = array(
+    '#title' => t('Height'),
+    '#title_display' => 'invisible',
+    '#type' => 'number',
+    '#default_value' => $editor->image_upload['max_dimensions']['height'],
+    '#size' => 8,
+    '#maxlength' => 8,
+    '#min' => 1,
+    '#max' => 99999,
+    '#placeholder' => 'height',
+    '#field_suffix' => 'pixels',
+    '#states' => $show_if_image_uploads_enabled,
+  );
+
+  return $form;
+}
diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module
index e9a44f06b8c9..3302f16cd866 100644
--- a/core/modules/editor/editor.module
+++ b/core/modules/editor/editor.module
@@ -8,6 +8,8 @@
 use Drupal\file\Entity\File;
 use Drupal\editor\Entity\Editor;
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\field\Field;
 
 /**
  * Implements hook_help().
@@ -384,3 +386,182 @@ function editor_pre_render_format($element) {
 
   return $element;
 }
+
+/**
+ * Implements hook_entity_insert().
+ */
+function editor_entity_insert(EntityInterface $entity) {
+  $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
+  foreach ($referenced_files_by_field as $field => $uuids) {
+    _editor_record_file_usage($uuids, $entity);
+  }
+}
+
+/**
+ * Implements hook_entity_update().
+ */
+function editor_entity_update(EntityInterface $entity) {
+  // On new revisions, all files are considered to be a new usage and no
+  // deletion of previous file usages are necessary.
+  if (!empty($entity->original) && $entity->getRevisionId() != $entity->original->getRevisionId()) {
+    $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
+    foreach ($referenced_files_by_field as $field => $uuids) {
+      _editor_record_file_usage($uuids, $entity);
+    }
+  }
+  // On modified revisions, detect which file references have been added (and
+  // record their usage) and which ones have been removed (delete their usage).
+  // File references that existed both in the previous version of the revision
+  // and in the new one don't need their usage to be updated.
+  else {
+    $original_uuids_by_field = _editor_get_file_uuids_by_field($entity->original);
+    $uuids_by_field = _editor_get_file_uuids_by_field($entity);
+
+    // Detect file usages that should be incremented.
+    foreach ($uuids_by_field as $field => $uuids) {
+      $added_files = array_diff($uuids_by_field[$field], $original_uuids_by_field[$field]);
+      _editor_record_file_usage($added_files, $entity);
+    }
+
+    // Detect file usages that should be decremented.
+    foreach ($original_uuids_by_field as $field => $uuids) {
+      $removed_files = array_diff($original_uuids_by_field[$field], $uuids_by_field[$field]);
+      _editor_delete_file_usage($removed_files, $entity, 1);
+    }
+  }
+}
+
+/**
+ * Implements hook_entity_delete().
+ */
+function editor_entity_delete(EntityInterface $entity) {
+  $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
+  foreach ($referenced_files_by_field as $field => $uuids) {
+    _editor_delete_file_usage($uuids, $entity, 0);
+  }
+}
+
+/**
+ * Implements hook_entity_revision_delete().
+ */
+function editor_entity_revision_delete(EntityInterface $entity) {
+  $referenced_files_by_field = _editor_get_file_uuids_by_field($entity);
+  foreach ($referenced_files_by_field as $field => $uuids) {
+    _editor_delete_file_usage($uuids, $entity, 1);
+  }
+}
+
+/**
+ * Records file usage of files referenced by processed text fields.
+ *
+ * Every referenced file that does not yet have the FILE_STATUS_PERMANENT state,
+ * will be given that state.
+ *
+ * @param array $uuids
+ *   An array of file entity UUIDs.
+ * @param EntityInterface $entity
+ *   An entity whose fields to inspect for file references.
+ */
+function _editor_record_file_usage(array $uuids, EntityInterface $entity) {
+  foreach ($uuids as $uuid) {
+    $file = entity_load_by_uuid('file', $uuid);
+    if ($file->status !== FILE_STATUS_PERMANENT) {
+      $file->status = FILE_STATUS_PERMANENT;
+      $file->save();
+    }
+    file_usage()->add($file, 'editor', $entity->entityType(), $entity->id());
+  }
+}
+
+/**
+ * Deletes file usage of files referenced by processed text fields.
+ *
+ * @param array $uuids
+ *   An array of file entity UUIDs.
+ * @param EntityInterface $entity
+ *   An entity whose fields to inspect for file references.
+ * @param $count
+ *   The number of references to delete. Should be 1 when deleting a single
+ *   revision and 0 when deleting an entity entirely.
+ *
+ * @see Drupal\file\FileUsage\FileUsageInterface::delete()
+ */
+function _editor_delete_file_usage(array $uuids, EntityInterface $entity, $count) {
+  foreach ($uuids as $uuid) {
+    $file = entity_load_by_uuid('file', $uuid);
+    file_usage()->delete($file, 'editor', $entity->entityType(), $entity->id(), $count);
+  }
+}
+
+/**
+ * Finds all files referenced (data-editor-file-uuid) by processed text fields.
+ *
+ * @param EntityInterface $entity
+ *   An entity whose fields to analyze.
+ *
+ * @return array
+ *   An array of file entity UUIDs.
+ */
+function _editor_get_file_uuids_by_field(EntityInterface $entity) {
+  $uuids = array();
+
+  $processed_text_fields = _editor_get_processed_text_fields($entity);
+  foreach ($processed_text_fields as $processed_text_field) {
+    $text = $entity->get($processed_text_field)->value;
+    $uuids[$processed_text_field] = _editor_parse_file_uuids($text);
+  }
+  return $uuids;
+}
+
+/**
+ * Determines the text fields on an entity that have text processing enabled.
+ *
+ * @param EntityInterface $entity
+ *   An entity whose fields to analyze.
+ *
+ * @return array
+ *   The names of the fields on this entity that have text processing enabled.
+ */
+function _editor_get_processed_text_fields(EntityInterface $entity) {
+  $properties = $entity->getPropertyDefinitions();
+  if (empty($properties)) {
+    return array();
+  }
+
+  // Find all configurable fields, because only they could have a
+  // text_processing setting.
+  $configurable_fields = array_keys(array_filter($properties, function ($definition) {
+    return isset($definition['configurable']) && $definition['configurable'] === TRUE;
+  }));
+  if (empty($configurable_fields)) {
+    return array();
+  }
+
+  // Only return fields that have text processing enabled.
+  return array_filter($configurable_fields, function ($field) use ($entity) {
+    $settings = Field::fieldInfo()
+      ->getInstance($entity->entityType(), $entity->bundle(), $field)
+      ->getFieldSettings();
+    return isset($settings['text_processing']) && $settings['text_processing'] === '1';
+  });
+}
+
+/**
+ * Parse an HTML snippet for any data-editor-file-uuid attributes.
+ *
+ * @param string $text
+ *   The partial (X)HTML snippet to load. Invalid markup will be corrected on
+ *   import.
+ *
+ * @return array
+ *   An array of all found UUIDs.
+ */
+function _editor_parse_file_uuids($text) {
+  $dom = filter_dom_load($text);
+  $xpath = new \DOMXPath($dom);
+  $uuids = array();
+  foreach ($xpath->query('//*[@data-editor-file-uuid]') as $node) {
+    $uuids[] = $node->getAttribute('data-editor-file-uuid');
+  }
+  return $uuids;
+}
diff --git a/core/modules/editor/lib/Drupal/editor/Entity/Editor.php b/core/modules/editor/lib/Drupal/editor/Entity/Editor.php
index 1183f80f8095..54949ec93016 100644
--- a/core/modules/editor/lib/Drupal/editor/Entity/Editor.php
+++ b/core/modules/editor/lib/Drupal/editor/Entity/Editor.php
@@ -47,12 +47,19 @@ class Editor extends ConfigEntityBase implements EditorInterface {
   public $editor;
 
   /**
-   * The array of settings for the text editor.
+   * The array of text editor plugin-specific settings for the text editor.
    *
    * @var array
    */
   public $settings = array();
 
+  /**
+   * The array of image upload settings for the text editor.
+   *
+   * @var array
+   */
+  public $image_upload = array();
+
   /**
    * Overrides Drupal\Core\Entity\Entity::id().
    */
diff --git a/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php b/core/modules/editor/lib/Drupal/editor/Form/EditorImageDialog.php
index b20f35f50c81..080721091caf 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,50 @@ 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->image_upload['dimensions'])) {
+      $max_dimensions = $editor->image_upload['dimensions']['max_width'] . 'x' . $editor->image_upload['dimensions']['max_height'];
+    }
+    else {
+      $max_dimensions = 0;
+    }
+    $max_filesize = min(parse_size($editor->image_upload['max_size']), file_upload_max_size());
+
+    $existing_file = isset($input['data-editor-file-uuid']) ? entity_load_by_uuid('file', $input['data-editor-file-uuid']) : NULL;
+    $fid = $existing_file ? $existing_file->id() : NULL;
+
+    $form['fid'] = array(
+      '#title' => t('Image'),
+      '#type' => 'managed_file',
+      '#upload_location' => $editor->image_upload['scheme'] . '://' .$editor->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 image uploads enabled, show a managed_file form item,
+    // otherwise show a (file URL) text form item.
+    if ($editor->image_upload['status'] === '1') {
+      $form['attributes']['src']['#access'] = FALSE;
+    }
+    else {
+      $form['fid']['#access'] = FALSE;
+    }
+
     $form['attributes']['alt'] = array(
       '#title' => t('Alternative text'),
       '#type' => 'textfield',
@@ -120,6 +156,14 @@ 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-editor-file-uuid
+    // attributes.
+    if (!empty($form_state['values']['fid'][0])) {
+      $file = file_load($form_state['values']['fid'][0]);
+      $form_state['values']['attributes']['src'] = file_create_url($file->getFileUri());
+      $form_state['values']['attributes']['data-editor-file-uuid'] = $file->uuid();
+    }
+
     if (form_get_errors()) {
       unset($form['#prefix'], $form['#suffix']);
       $status_messages = array('#theme' => 'status_messages');
diff --git a/core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php b/core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php
new file mode 100644
index 000000000000..80384ae0f3a1
--- /dev/null
+++ b/core/modules/editor/lib/Drupal/editor/Tests/EditorFileUsageTest.php
@@ -0,0 +1,117 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\editor\Tests\EditorFileUsageTest.
+ */
+
+namespace Drupal\editor\Tests;
+
+use Drupal\simpletest\DrupalUnitTestBase;
+
+/**
+ * Unit tests for editor.module's entity hooks to track file usage.
+ */
+class EditorFileUsageTest extends DrupalUnitTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array('system', 'editor', 'editor_test', 'filter', 'node', 'entity', 'field', 'text', 'field_sql_storage', 'file');
+
+  public static function getInfo() {
+    return array(
+      'name' => 'Text Editor file usage',
+      'description' => 'Tests tracking of file usage by the Text Editor module.',
+      'group' => 'Text Editor',
+    );
+  }
+
+  function setUp() {
+    parent::setUp();
+    $this->installSchema('system', 'url_alias');
+    $this->installSchema('node', 'node');
+    $this->installSchema('node', 'node_access');
+    $this->installSchema('node', 'node_field_data');
+    $this->installSchema('node', 'node_field_revision');
+    $this->installSchema('file', 'file_managed');
+    $this->installSchema('file', 'file_usage');
+
+    // Add text formats.
+    $filtered_html_format = entity_create('filter_format', array(
+      'format' => 'filtered_html',
+      'name' => 'Filtered HTML',
+      'weight' => 0,
+      'filters' => array(),
+    ));
+    $filtered_html_format->save();
+
+    // Set up text editor.
+    $editor = entity_create('editor', array(
+      'format' => 'filtered_html',
+      'editor' => 'unicorn',
+    ));
+    $editor->save();
+
+    // Create a node type for testing.
+    $type = entity_create('node_type', array('type' => 'page', 'name' => 'page'));
+    $type->save();
+  }
+
+  /**
+   * Tests the configurable text editor manager.
+   */
+  function testEditorEntityHooks() {
+    $image = entity_create('file', array());
+    $image->setFileUri('core/misc/druplicon.png');
+    $image->setFilename(drupal_basename($image->getFileUri()));
+    $image->save();
+    $this->assertIdentical(array(), file_usage()->listUsage($image), 'The image has zero usages.');
+
+    // Test editor_entity_insert(): increment.
+    $node = entity_create('node', array(
+      'type' => 'page',
+      'title' => 'test',
+      'body' => array(
+        'value' => '<p>Hello, world!</p><img src="awesome-llama.jpg" data-editor-file-uuid="' . $image->uuid() . '" />',
+        'format' => 'filtered_html',
+      )
+    ));
+    $node->save();
+    $this->assertIdentical(array('editor' => array('node' => array(1 => '1'))), file_usage()->listUsage($image), 'The image has 1 usage.');
+
+    // Test editor_entity_update(): increment, twice, by creating new revisions.
+    $node->setNewRevision(TRUE);
+    $node->save();
+    $second_revision_id = $node->getRevisionId();
+    $node->setNewRevision(TRUE);
+    $node->save();
+    $this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), file_usage()->listUsage($image), 'The image has 3 usages.');
+
+    // Test hook_entity_update(): decrement, by modifying the last revision:
+    // remove the data- attribute from the body field.
+    $body = $node->get('body')->offsetGet(0)->get('value');
+    $original_value = $body->getValue();
+    $new_value = str_replace('data-editor-file-uuid', 'data-editor-file-uuid-modified', $original_value);
+    $body->setValue($new_value);
+    $node->save();
+    $this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), file_usage()->listUsage($image), 'The image has 2 usages.');
+
+    // Test hook_entity_update(): increment, by modifying the last revision:
+    // readd the data- attribute to the body field.
+    $node->get('body')->offsetGet(0)->get('value')->setValue($original_value);
+    $node->save();
+    $this->assertIdentical(array('editor' => array('node' => array(1 => '3'))), file_usage()->listUsage($image), 'The image has 3 usages.');
+
+    // Test editor_entity_revision_delete(): decrement, by deleting a revision.
+    entity_revision_delete('node', $second_revision_id);
+    $this->assertIdentical(array('editor' => array('node' => array(1 => '2'))), file_usage()->listUsage($image), 'The image has 2 usages.');
+
+    // Test editor_entity_delete().
+    $node->delete();
+    $this->assertIdentical(array(), file_usage()->listUsage($image), 'The image has zero usages again.');
+  }
+
+}
diff --git a/core/modules/text/lib/Drupal/text/TextProcessed.php b/core/modules/text/lib/Drupal/text/TextProcessed.php
index 9a1910f8e532..352420de387f 100644
--- a/core/modules/text/lib/Drupal/text/TextProcessed.php
+++ b/core/modules/text/lib/Drupal/text/TextProcessed.php
@@ -88,4 +88,5 @@ public function setValue($value, $notify = TRUE) {
       // throw new ReadOnlyException('Unable to set a computed property.');
     }
   }
+
 }
diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/area/Text.php b/core/modules/views/lib/Drupal/views/Plugin/views/area/Text.php
index 645e10df10e3..71124a203238 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/views/area/Text.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/views/area/Text.php
@@ -39,7 +39,7 @@ public function buildOptionsForm(&$form, &$form_state) {
       '#default_value' => $this->options['content'],
       '#rows' => 6,
       '#format' => isset($this->options['format']) ? $this->options['format'] : filter_default_format(),
-      '#wysiwyg' => FALSE,
+      '#editor' => FALSE,
     );
   }
 
diff --git a/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/InputRequired.php b/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/InputRequired.php
index baf851660b5a..4cf727659736 100644
--- a/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/InputRequired.php
+++ b/core/modules/views/lib/Drupal/views/Plugin/views/exposed_form/InputRequired.php
@@ -41,7 +41,7 @@ public function buildOptionsForm(&$form, &$form_state) {
       '#description' => t('Text to display instead of results until the user selects and applies an exposed filter.'),
       '#default_value' => $this->options['text_input_required'],
       '#format' => isset($this->options['text_input_required_format']) ? $this->options['text_input_required_format'] : filter_default_format(),
-      '#wysiwyg' => FALSE,
+      '#editor' => FALSE,
     );
   }
 
diff --git a/core/profiles/standard/config/editor.editor.basic_html.yml b/core/profiles/standard/config/editor.editor.basic_html.yml
index cf4a60011f38..e3406852cac0 100644
--- a/core/profiles/standard/config/editor.editor.basic_html.yml
+++ b/core/profiles/standard/config/editor.editor.basic_html.yml
@@ -20,5 +20,13 @@ settings:
   plugins:
     stylescombo:
       styles: ''
+image_upload:
+  status: '1'
+  scheme: public
+  directory: inline-images
+  max_size: ''
+  max_dimensions:
+    width: ''
+    height: ''
 status: '1'
 langcode: und
diff --git a/core/profiles/standard/config/editor.editor.full_html.yml b/core/profiles/standard/config/editor.editor.full_html.yml
index c0111f9d7bff..eb01c1cb2661 100644
--- a/core/profiles/standard/config/editor.editor.full_html.yml
+++ b/core/profiles/standard/config/editor.editor.full_html.yml
@@ -30,5 +30,13 @@ settings:
   plugins:
     stylescombo:
       styles: ''
+image_upload:
+  status: '1'
+  scheme: public
+  directory: inline-images
+  max_size: ''
+  max_dimensions:
+    width: ''
+    height: ''
 status: '1'
 langcode: und
-- 
GitLab